diff --git a/Makefile b/Makefile index 408c2af3ee..b354ec2af6 100644 --- a/Makefile +++ b/Makefile @@ -1183,6 +1183,8 @@ jsc-copy-headers: cp $(WEBKIT_DIR)/Source/JavaScriptCore/runtime/SymbolObject.h $(WEBKIT_RELEASE_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/SymbolObject.h cp $(WEBKIT_DIR)/Source/JavaScriptCore/runtime/JSGenerator.h $(WEBKIT_RELEASE_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/JSGenerator.h cp $(WEBKIT_DIR)/Source/JavaScriptCore/bytecode/UnlinkedFunctionCodeBlock.h $(WEBKIT_RELEASE_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/UnlinkedFunctionCodeBlock.h + cp $(WEBKIT_DIR)/Source/JavaScriptCore/bytecode/GlobalCodeBlock.h $(WEBKIT_RELEASE_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/GlobalCodeBlock.h + cp $(WEBKIT_DIR)/Source/JavaScriptCore/bytecode/ProgramCodeBlock.h $(WEBKIT_RELEASE_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/ProgramCodeBlock.h cp $(WEBKIT_DIR)/Source/JavaScriptCore/runtime/AggregateError.h $(WEBKIT_RELEASE_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/AggregateError.h cp $(WEBKIT_DIR)/Source/JavaScriptCore/API/JSWeakValue.h $(WEBKIT_RELEASE_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/JSWeakValue.h find $(WEBKIT_RELEASE_DIR)/JavaScriptCore/Headers/JavaScriptCore/ -name "*.h" -exec cp {} $(WEBKIT_RELEASE_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/ \; @@ -1234,6 +1236,8 @@ jsc-copy-headers-debug: cp $(WEBKIT_DIR)/Source/JavaScriptCore/runtime/SymbolObject.h $(WEBKIT_DEBUG_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/SymbolObject.h cp $(WEBKIT_DIR)/Source/JavaScriptCore/runtime/JSGenerator.h $(WEBKIT_DEBUG_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/JSGenerator.h cp $(WEBKIT_DIR)/Source/JavaScriptCore/bytecode/UnlinkedFunctionCodeBlock.h $(WEBKIT_DEBUG_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/UnlinkedFunctionCodeBlock.h + cp $(WEBKIT_DIR)/Source/JavaScriptCore/bytecode/GlobalCodeBlock.h $(WEBKIT_DEBUG_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/GlobalCodeBlock.h + cp $(WEBKIT_DIR)/Source/JavaScriptCore/bytecode/ProgramCodeBlock.h $(WEBKIT_DEBUG_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/ProgramCodeBlock.h cp $(WEBKIT_DIR)/Source/JavaScriptCore/runtime/AggregateError.h $(WEBKIT_DEBUG_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/AggregateError.h cp $(WEBKIT_DIR)/Source/JavaScriptCore/API/JSWeakValue.h $(WEBKIT_DEBUG_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/JSWeakValue.h find $(WEBKIT_DEBUG_DIR)/JavaScriptCore/Headers/JavaScriptCore/ -name "*.h" -exec cp {} $(WEBKIT_DEBUG_DIR)/JavaScriptCore/PrivateHeaders/JavaScriptCore/ \; diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index 17ed2fd294..e9fe5f720a 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use") option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading") if(NOT WEBKIT_VERSION) - set(WEBKIT_VERSION e26b186170c48a40d872c05a5ba61821a3f31196) + set(WEBKIT_VERSION 53d4176ddc98ba721e50355826f58ec758766fa8) endif() string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX) diff --git a/src/bun.js/bindings/NodeVM.cpp b/src/bun.js/bindings/NodeVM.cpp index 0a7b15f4ca..8146a46774 100644 --- a/src/bun.js/bindings/NodeVM.cpp +++ b/src/bun.js/bindings/NodeVM.cpp @@ -42,6 +42,12 @@ #include "NodeValidator.h" #include "JavaScriptCore/JSCInlines.h" +#include "JavaScriptCore/CodeCache.h" +#include "JavaScriptCore/BytecodeCacheError.h" +#include "wtf/FileHandle.h" + +#include "JavaScriptCore/ProgramCodeBlock.h" +#include "JavaScriptCore/JIT.h" namespace Bun { using namespace WebCore; @@ -52,6 +58,9 @@ using namespace WebCore; static JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, const ArgList& args, const SourceOrigin& sourceOrigin, const String& fileName = String(), JSC::SourceTaintedOrigin sourceTaintOrigin = JSC::SourceTaintedOrigin::Untainted, TextPosition position = TextPosition(), JSC::JSScope* scope = nullptr); static String stringifyAnonymousFunction(JSGlobalObject* globalObject, const ArgList& args, ThrowScope& scope, int* outOffset); +static RefPtr getBytecode(JSGlobalObject* globalObject, JSC::ProgramExecutable* executable, JSC::SourceCode source); +static JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, JSC::SourceCode source); + NodeVMGlobalObject* createContextImpl(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* sandbox); /// For some reason Node has this error message with a grammar error and we have to match it so the tests pass: @@ -67,6 +76,328 @@ JSC::EncodedJSValue INVALID_ARG_VALUE_VM_VARIATION(JSC::ThrowScope& throwScope, return {}; } +class BaseOptions { +public: + String filename = String(); + OrdinalNumber lineOffset; + OrdinalNumber columnOffset; + bool failed; + + bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) + { + JSObject* options = nullptr; + bool any = false; + + if (!optionsArg.isUndefined()) { + if (optionsArg.isObject()) { + options = asObject(optionsArg); + } else { + auto _ = ERR::INVALID_ARG_TYPE(scope, globalObject, "options"_s, "object"_s, optionsArg); + return false; + } + + if (JSValue filenameOpt = options->getIfPropertyExists(globalObject, builtinNames(vm).filenamePublicName())) { + if (filenameOpt.isString()) { + this->filename = filenameOpt.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, false); + any = true; + } else if (!filenameOpt.isUndefined()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.filename"_s, "string"_s, filenameOpt); + return false; + } + } else { + this->filename = "evalmachine."_s; + } + + if (JSValue lineOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "lineOffset"_s))) { + if (lineOffsetOpt.isAnyInt()) { + if (!lineOffsetOpt.isInt32()) { + ERR::OUT_OF_RANGE(scope, globalObject, "options.lineOffset"_s, std::numeric_limits().min(), std::numeric_limits().max(), lineOffsetOpt); + return false; + } + this->lineOffset = OrdinalNumber::fromZeroBasedInt(lineOffsetOpt.asInt32()); + any = true; + } else if (lineOffsetOpt.isNumber()) { + ERR::OUT_OF_RANGE(scope, globalObject, "options.lineOffset"_s, "an integer"_s, lineOffsetOpt); + return false; + } else if (!lineOffsetOpt.isUndefined()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.lineOffset"_s, "number"_s, lineOffsetOpt); + return false; + } + } + + if (JSValue columnOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "columnOffset"_s))) { + if (columnOffsetOpt.isAnyInt()) { + if (!columnOffsetOpt.isInt32()) { + ERR::OUT_OF_RANGE(scope, globalObject, "options.columnOffset"_s, std::numeric_limits().min(), std::numeric_limits().max(), columnOffsetOpt); + return false; + } + int columnOffsetValue = columnOffsetOpt.asInt32(); + + this->columnOffset = OrdinalNumber::fromZeroBasedInt(columnOffsetValue); + any = true; + } else if (columnOffsetOpt.isNumber()) { + ERR::OUT_OF_RANGE(scope, globalObject, "options.columnOffset"_s, "an integer"_s, columnOffsetOpt); + return false; + } else if (!columnOffsetOpt.isUndefined()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.columnOffset"_s, "number"_s, columnOffsetOpt); + return false; + } + } + } + + return any; + } + + bool validateProduceCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, bool& outProduceCachedData) + { + JSValue produceCachedDataOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "produceCachedData"_s)); + if (produceCachedDataOpt && !produceCachedDataOpt.isUndefined()) { + RETURN_IF_EXCEPTION(scope, {}); + if (!produceCachedDataOpt.isBoolean()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.produceCachedData"_s, "boolean"_s, produceCachedDataOpt); + return false; + } + outProduceCachedData = produceCachedDataOpt.asBoolean(); + return true; + } + return false; + } + + bool validateCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, std::vector& outCachedData) + { + JSValue cachedDataOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "cachedData"_s)); + if (cachedDataOpt && !cachedDataOpt.isUndefined()) { + RETURN_IF_EXCEPTION(scope, {}); + if (!cachedDataOpt.isCell()) { + ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataOpt); + return false; + } + + // If it's a cell, verify it's a Buffer, TypedArray or DataView + if (cachedDataOpt.isCell()) { + bool isValidType = false; + + if (auto* arrayBufferView = JSC::jsDynamicCast(cachedDataOpt)) { + if (!arrayBufferView->isDetached()) { + std::span span = arrayBufferView->span(); + outCachedData = { span.begin(), span.end() }; + isValidType = true; + } + } else if (auto* arrayBuffer = JSC::jsDynamicCast(cachedDataOpt); arrayBuffer && arrayBuffer->impl()) { + std::span span = arrayBuffer->impl()->span(); + outCachedData = { span.begin(), span.end() }; + isValidType = true; + } + + if (!isValidType) { + ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataOpt); + return false; + } + + return true; + } + } + return false; + } + + bool validateTimeout(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, std::optional& outTimeout) + { + JSValue timeoutOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "timeout"_s)); + if (timeoutOpt && !timeoutOpt.isUndefined()) { + if (!timeoutOpt.isNumber()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.timeout"_s, "number"_s, timeoutOpt); + return false; + } + + ssize_t timeoutValue; + V::validateInteger(scope, globalObject, timeoutOpt, "options.timeout"_s, jsNumber(1), jsNumber(std::numeric_limits().max()), &timeoutValue); + RETURN_IF_EXCEPTION(scope, {}); + + outTimeout = timeoutValue; + return true; + } + return false; + } +}; + +class ScriptOptions : public BaseOptions { +public: + std::optional timeout = std::nullopt; + bool produceCachedData = false; + std::vector cachedData; + + bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) + { + bool any = BaseOptions::fromJS(globalObject, vm, scope, optionsArg); + RETURN_IF_EXCEPTION(scope, false); + + if (!optionsArg.isUndefined() && !optionsArg.isString()) { + JSObject* options = asObject(optionsArg); + + // Validate contextName and contextOrigin are strings + if (JSValue contextNameOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextName"_s))) { + if (!contextNameOpt.isUndefined() && !contextNameOpt.isString()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextName"_s, "string"_s, contextNameOpt); + return false; + } + any = true; + } + RETURN_IF_EXCEPTION(scope, false); + + if (JSValue contextOriginOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextOrigin"_s))) { + if (!contextOriginOpt.isUndefined() && !contextOriginOpt.isString()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextOrigin"_s, "string"_s, contextOriginOpt); + return false; + } + any = true; + } + RETURN_IF_EXCEPTION(scope, false); + + if (validateTimeout(globalObject, vm, scope, options, this->timeout)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + + if (validateProduceCachedData(globalObject, vm, scope, options, this->produceCachedData)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + + if (validateCachedData(globalObject, vm, scope, options, this->cachedData)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + } + + return any; + } +}; + +class RunningScriptOptions : public BaseOptions { +public: + bool displayErrors = true; + std::optional timeout = std::nullopt; + bool breakOnSigint = false; + + bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) + { + bool any = BaseOptions::fromJS(globalObject, vm, scope, optionsArg); + RETURN_IF_EXCEPTION(scope, false); + + if (!optionsArg.isUndefined() && !optionsArg.isString()) { + JSObject* options = asObject(optionsArg); + + if (JSValue displayErrorsOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "displayErrors"_s))) { + RETURN_IF_EXCEPTION(scope, false); + if (!displayErrorsOpt.isBoolean()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.displayErrors"_s, "boolean"_s, displayErrorsOpt); + return false; + } + this->displayErrors = displayErrorsOpt.asBoolean(); + any = true; + } + + if (validateTimeout(globalObject, vm, scope, options, this->timeout)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + + if (JSValue breakOnSigintOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "breakOnSigint"_s))) { + RETURN_IF_EXCEPTION(scope, false); + if (!breakOnSigintOpt.isBoolean()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, "options.breakOnSigint"_s, "boolean"_s, breakOnSigintOpt); + return false; + } + this->breakOnSigint = breakOnSigintOpt.asBoolean(); + any = true; + } + } + + return any; + } +}; + +class CompileFunctionOptions : public BaseOptions { +public: + std::vector cachedData; + JSGlobalObject* parsingContext = nullptr; + JSValue contextExtensions; + bool produceCachedData = false; + + bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) + { + this->parsingContext = globalObject; + bool any = BaseOptions::fromJS(globalObject, vm, scope, optionsArg); + RETURN_IF_EXCEPTION(scope, false); + + if (!optionsArg.isUndefined() && !optionsArg.isString()) { + JSObject* options = asObject(optionsArg); + + if (validateProduceCachedData(globalObject, vm, scope, options, this->produceCachedData)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + + if (validateCachedData(globalObject, vm, scope, options, this->cachedData)) { + RETURN_IF_EXCEPTION(scope, false); + any = true; + } + + JSValue parsingContextValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "parsingContext"_s)); + RETURN_IF_EXCEPTION(scope, {}); + + if (!parsingContextValue.isEmpty() && !parsingContextValue.isUndefined()) { + if (parsingContextValue.isNull() || !parsingContextValue.isObject()) + return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); + + JSObject* context = asObject(parsingContextValue); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSValue scopeValue = zigGlobalObject->vmModuleContextMap()->get(context); + + if (scopeValue.isUndefined()) + return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); + + parsingContext = jsDynamicCast(scopeValue); + if (!parsingContext) + return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); + + any = true; + } + + // Handle contextExtensions option + JSValue contextExtensionsValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextExtensions"_s)); + RETURN_IF_EXCEPTION(scope, {}); + + if (!contextExtensionsValue.isEmpty() && !contextExtensionsValue.isUndefined()) { + if (contextExtensionsValue.isNull() || !contextExtensionsValue.isObject()) + return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); + + if (auto* contextExtensionsObject = asObject(contextExtensionsValue)) { + if (!isArray(globalObject, contextExtensionsObject)) + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); + + // Validate that all items in the array are objects + auto* contextExtensionsArray = jsCast(contextExtensionsValue); + unsigned length = contextExtensionsArray->length(); + for (unsigned i = 0; i < length; i++) { + JSValue extension = contextExtensionsArray->getIndexQuickly(i); + if (!extension.isObject()) + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions[0]"_s, "object"_s, extension); + } + } else { + return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); + } + + this->contextExtensions = contextExtensionsValue; + any = true; + } + } + + return any; + } +}; + class NodeVMScriptConstructor final : public JSC::InternalFunction { public: using Base = JSC::InternalFunction; @@ -91,7 +422,7 @@ class NodeVMScript final : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; - static NodeVMScript* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::SourceCode source); + static NodeVMScript* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::SourceCode source, ScriptOptions options); DECLARE_EXPORT_INFO; template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) @@ -114,17 +445,34 @@ public: static JSObject* createPrototype(VM& vm, JSGlobalObject* globalObject); + JSC::ProgramExecutable* createExecutable(); + void cacheBytecode(); + JSC::JSUint8Array* getBytecodeBuffer(); + const JSC::SourceCode& source() const { return m_source; } + std::vector& cachedData() { return m_options.cachedData; } + RefPtr cachedBytecode() const { return m_cachedBytecode; } + JSC::ProgramExecutable* cachedExecutable() const { return m_cachedExecutable.get(); } + bool cachedDataProduced() const { return m_cachedDataProduced; } + void cachedDataProduced(bool value) { m_cachedDataProduced = value; } + TriState cachedDataRejected() const { return m_cachedDataRejected; } + void cachedDataRejected(TriState value) { m_cachedDataRejected = value; } DECLARE_VISIT_CHILDREN; - mutable JSC::WriteBarrier m_cachedDirectExecutable; private: JSC::SourceCode m_source; + RefPtr m_cachedBytecode; + mutable JSC::WriteBarrier m_cachedBytecodeBuffer; + mutable JSC::WriteBarrier m_cachedExecutable; + ScriptOptions m_options; + bool m_cachedDataProduced = false; + TriState m_cachedDataRejected = TriState::Indeterminate; - NodeVMScript(JSC::VM& vm, JSC::Structure* structure, JSC::SourceCode source) + NodeVMScript(JSC::VM& vm, JSC::Structure* structure, JSC::SourceCode source, ScriptOptions options) : Base(vm, structure) , m_source(source) + , m_options(WTFMove(options)) { } @@ -378,329 +726,6 @@ void NodeVMGlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_sandbox); } -class BaseOptions { -public: - String filename = String(); - OrdinalNumber lineOffset; - OrdinalNumber columnOffset; - bool failed; - - bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) - { - JSObject* options = nullptr; - bool any = false; - - if (!optionsArg.isUndefined()) { - if (optionsArg.isObject()) { - options = asObject(optionsArg); - } else { - auto _ = ERR::INVALID_ARG_TYPE(scope, globalObject, "options"_s, "object"_s, optionsArg); - return false; - } - - if (JSValue filenameOpt = options->getIfPropertyExists(globalObject, builtinNames(vm).filenamePublicName())) { - if (filenameOpt.isString()) { - this->filename = filenameOpt.toWTFString(globalObject); - RETURN_IF_EXCEPTION(scope, false); - any = true; - } else if (!filenameOpt.isUndefined()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.filename"_s, "string"_s, filenameOpt); - return false; - } - } else { - this->filename = "evalmachine."_s; - } - - if (JSValue lineOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "lineOffset"_s))) { - if (lineOffsetOpt.isAnyInt()) { - if (!lineOffsetOpt.isInt32()) { - ERR::OUT_OF_RANGE(scope, globalObject, "options.lineOffset"_s, std::numeric_limits().min(), std::numeric_limits().max(), lineOffsetOpt); - return false; - } - this->lineOffset = OrdinalNumber::fromZeroBasedInt(lineOffsetOpt.asInt32()); - any = true; - } else if (lineOffsetOpt.isNumber()) { - ERR::OUT_OF_RANGE(scope, globalObject, "options.lineOffset"_s, "an integer"_s, lineOffsetOpt); - return false; - } else if (!lineOffsetOpt.isUndefined()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.lineOffset"_s, "number"_s, lineOffsetOpt); - return false; - } - } - - if (JSValue columnOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "columnOffset"_s))) { - if (columnOffsetOpt.isAnyInt()) { - if (!columnOffsetOpt.isInt32()) { - ERR::OUT_OF_RANGE(scope, globalObject, "options.columnOffset"_s, std::numeric_limits().min(), std::numeric_limits().max(), columnOffsetOpt); - return false; - } - int columnOffsetValue = columnOffsetOpt.asInt32(); - - this->columnOffset = OrdinalNumber::fromZeroBasedInt(columnOffsetValue); - any = true; - } else if (columnOffsetOpt.isNumber()) { - ERR::OUT_OF_RANGE(scope, globalObject, "options.columnOffset"_s, "an integer"_s, columnOffsetOpt); - return false; - } else if (!columnOffsetOpt.isUndefined()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.columnOffset"_s, "number"_s, columnOffsetOpt); - return false; - } - } - } - - return any; - } - - bool validateProduceCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, bool* outProduceCachedData) - { - JSValue produceCachedDataOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "produceCachedData"_s)); - if (produceCachedDataOpt && !produceCachedDataOpt.isUndefined()) { - RETURN_IF_EXCEPTION(scope, {}); - if (!produceCachedDataOpt.isBoolean()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.produceCachedData"_s, "boolean"_s, produceCachedDataOpt); - return false; - } - *outProduceCachedData = produceCachedDataOpt.asBoolean(); - return true; - } - return false; - } - - bool validateCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options) - { - JSValue cachedDataOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "cachedData"_s)); - if (cachedDataOpt && !cachedDataOpt.isUndefined()) { - RETURN_IF_EXCEPTION(scope, {}); - if (!cachedDataOpt.isCell()) { - ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataOpt); - return false; - } - - // If it's a cell, verify it's a Buffer, TypedArray, or DataView - if (cachedDataOpt.isCell()) { - JSCell* cell = cachedDataOpt.asCell(); - bool isValidType = false; - - // Check if it's a Buffer, TypedArray, or DataView - if (cell->inherits() || cell->inherits()) { - isValidType = true; - } else if (JSC::JSArrayBufferView* view = JSC::jsDynamicCast(cachedDataOpt)) { - isValidType = !view->isDetached(); - } - - if (!isValidType) { - ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataOpt); - return false; - } - return true; - - // TODO: actually use it - // this->cachedData = true; - } - } - return false; - } - - bool validateTimeout(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, std::optional* outTimeout) - { - JSValue timeoutOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "timeout"_s)); - if (timeoutOpt && !timeoutOpt.isUndefined()) { - if (!timeoutOpt.isNumber()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.timeout"_s, "number"_s, timeoutOpt); - return false; - } - - ssize_t timeoutValue; - V::validateInteger(scope, globalObject, timeoutOpt, "options.timeout"_s, jsNumber(1), jsNumber(std::numeric_limits().max()), &timeoutValue); - RETURN_IF_EXCEPTION(scope, {}); - - *outTimeout = timeoutValue; - return true; - } - return false; - } -}; - -class ScriptOptions : public BaseOptions { -public: - bool importModuleDynamically = false; - std::optional timeout = std::nullopt; - bool cachedData = false; - bool produceCachedData = false; - - bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) - { - bool any = BaseOptions::fromJS(globalObject, vm, scope, optionsArg); - RETURN_IF_EXCEPTION(scope, false); - - if (!optionsArg.isUndefined() && !optionsArg.isString()) { - JSObject* options = asObject(optionsArg); - - // Validate contextName and contextOrigin are strings - if (JSValue contextNameOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextName"_s))) { - if (!contextNameOpt.isUndefined() && !contextNameOpt.isString()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextName"_s, "string"_s, contextNameOpt); - return false; - } - any = true; - } - - if (JSValue contextOriginOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextOrigin"_s))) { - if (!contextOriginOpt.isUndefined() && !contextOriginOpt.isString()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextOrigin"_s, "string"_s, contextOriginOpt); - return false; - } - any = true; - } - - if (validateTimeout(globalObject, vm, scope, options, &this->timeout)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - } - - if (validateProduceCachedData(globalObject, vm, scope, options, &this->produceCachedData)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - } - - if (validateCachedData(globalObject, vm, scope, options)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - // TODO: actually use it - this->cachedData = true; - } - } - - return any; - } -}; - -class RunningScriptOptions : public BaseOptions { -public: - bool displayErrors = true; - std::optional timeout = std::nullopt; - bool breakOnSigint = false; - - bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) - { - bool any = BaseOptions::fromJS(globalObject, vm, scope, optionsArg); - RETURN_IF_EXCEPTION(scope, false); - - if (!optionsArg.isUndefined() && !optionsArg.isString()) { - JSObject* options = asObject(optionsArg); - - if (JSValue displayErrorsOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "displayErrors"_s))) { - RETURN_IF_EXCEPTION(scope, false); - if (!displayErrorsOpt.isBoolean()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.displayErrors"_s, "boolean"_s, displayErrorsOpt); - return false; - } - this->displayErrors = displayErrorsOpt.asBoolean(); - any = true; - } - - if (validateTimeout(globalObject, vm, scope, options, &this->timeout)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - } - - if (JSValue breakOnSigintOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "breakOnSigint"_s))) { - RETURN_IF_EXCEPTION(scope, false); - if (!breakOnSigintOpt.isBoolean()) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "options.breakOnSigint"_s, "boolean"_s, breakOnSigintOpt); - return false; - } - this->breakOnSigint = breakOnSigintOpt.asBoolean(); - any = true; - } - } - - return any; - } -}; - -class CompileFunctionOptions : public BaseOptions { -public: - bool cachedData = false; - bool produceCachedData; - JSGlobalObject* parsingContext; - JSValue contextExtensions; - - bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) - { - this->parsingContext = globalObject; - bool any = BaseOptions::fromJS(globalObject, vm, scope, optionsArg); - RETURN_IF_EXCEPTION(scope, false); - - if (!optionsArg.isUndefined() && !optionsArg.isString()) { - JSObject* options = asObject(optionsArg); - - if (validateProduceCachedData(globalObject, vm, scope, options, &this->produceCachedData)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - } - - if (validateCachedData(globalObject, vm, scope, options)) { - RETURN_IF_EXCEPTION(scope, false); - any = true; - // TODO: actually use it - this->cachedData = true; - } - - JSValue parsingContextValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "parsingContext"_s)); - RETURN_IF_EXCEPTION(scope, {}); - - if (!parsingContextValue.isEmpty() && !parsingContextValue.isUndefined()) { - if (parsingContextValue.isNull() || !parsingContextValue.isObject()) - return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); - - JSObject* context = asObject(parsingContextValue); - auto* zigGlobalObject = defaultGlobalObject(globalObject); - JSValue scopeValue = zigGlobalObject->vmModuleContextMap()->get(context); - - if (scopeValue.isUndefined()) - return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); - - parsingContext = jsDynamicCast(scopeValue); - if (!parsingContext) - return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.parsingContext"_s, "Context"_s, parsingContextValue); - - any = true; - } - - // Handle contextExtensions option - JSValue contextExtensionsValue = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "contextExtensions"_s)); - RETURN_IF_EXCEPTION(scope, {}); - - if (!contextExtensionsValue.isEmpty() && !contextExtensionsValue.isUndefined()) { - if (contextExtensionsValue.isNull() || !contextExtensionsValue.isObject()) - return ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); - - if (auto* contextExtensionsObject = asObject(contextExtensionsValue)) { - if (!isArray(globalObject, contextExtensionsObject)) - return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); - - // Validate that all items in the array are objects - auto* contextExtensionsArray = jsCast(contextExtensionsValue); - unsigned length = contextExtensionsArray->length(); - for (unsigned i = 0; i < length; i++) { - JSValue extension = contextExtensionsArray->getIndexQuickly(i); - if (!extension.isObject()) - return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions[0]"_s, "object"_s, extension); - } - } else { - return ERR::INVALID_ARG_TYPE(scope, globalObject, "options.contextExtensions"_s, "Array"_s, contextExtensionsValue); - } - - this->contextExtensions = contextExtensionsValue; - any = true; - } - } - - return any; - } -}; - static EncodedJSValue constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newTarget = JSValue()) { @@ -741,8 +766,51 @@ constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newT JSC::StringSourceProvider::create(sourceString, JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename)), options.filename, JSC::SourceTaintedOrigin::Untainted, TextPosition(options.lineOffset, options.columnOffset)), options.lineOffset.zeroBasedInt(), options.columnOffset.zeroBasedInt()); RETURN_IF_EXCEPTION(scope, {}); - NodeVMScript* script = NodeVMScript::create(vm, globalObject, structure, source); - return JSValue::encode(JSValue(script)); + + const bool produceCachedData = options.produceCachedData; + auto filename = options.filename; + + NodeVMScript* script = NodeVMScript::create(vm, globalObject, structure, source, WTFMove(options)); + + std::vector& cachedData = script->cachedData(); + + if (!cachedData.empty()) { + JSC::ProgramExecutable* executable = script->cachedExecutable(); + if (!executable) { + executable = script->createExecutable(); + } + ASSERT(executable); + + JSC::LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? JSC::TaintedByWithScopeLexicallyScopedFeature : JSC::NoLexicallyScopedFeatures; + JSC::SourceCodeKey key(source, {}, JSC::SourceCodeType::ProgramType, lexicallyScopedFeatures, JSC::JSParserScriptMode::Classic, JSC::DerivedContextType::None, JSC::EvalContextType::None, false, {}, std::nullopt); + Ref cachedBytecode = JSC::CachedBytecode::create(std::span(cachedData), nullptr, {}); + JSC::UnlinkedProgramCodeBlock* unlinkedBlock = JSC::decodeCodeBlock(vm, key, WTFMove(cachedBytecode)); + + if (!unlinkedBlock) { + script->cachedDataRejected(TriState::True); + } else { + JSC::JSScope* jsScope = globalObject->globalScope(); + JSC::CodeBlock* codeBlock = nullptr; + { + // JSC::ProgramCodeBlock::create() requires GC to be deferred. + DeferGC deferGC(vm); + codeBlock = JSC::ProgramCodeBlock::create(vm, executable, unlinkedBlock, jsScope); + } + JSC::CompilationResult compilationResult = JIT::compileSync(vm, codeBlock, JITCompilationEffort::JITCompilationCanFail); + if (compilationResult != JSC::CompilationResult::CompilationFailed) { + executable->installCode(codeBlock); + script->cachedDataRejected(TriState::False); + } else { + script->cachedDataRejected(TriState::True); + } + } + } else if (produceCachedData) { + script->cacheBytecode(); + // TODO(@heimskr): is there ever a case where bytecode production fails? + script->cachedDataProduced(true); + } + + return JSValue::encode(script); } static bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr exception, ThrowScope& throwScope) @@ -821,15 +889,19 @@ JSC_DEFINE_HOST_FUNCTION(scriptConstructorConstruct, (JSGlobalObject * globalObj return constructScript(globalObject, callFrame, callFrame->newTarget()); } -JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataRejected, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - return JSValue::encode(jsBoolean(true)); // TODO -} JSC_DEFINE_HOST_FUNCTION(scriptCreateCachedData, (JSGlobalObject * globalObject, CallFrame* callFrame)) { auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - return throwVMError(globalObject, scope, "TODO: Script.createCachedData"_s); + + JSValue thisValue = callFrame->thisValue(); + auto* script = jsDynamicCast(thisValue); + if (UNLIKELY(!script)) { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + const JSC::SourceCode& source = script->source(); + return createCachedData(globalObject, source); } JSC_DEFINE_HOST_FUNCTION(scriptRunInContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -923,6 +995,56 @@ JSC_DEFINE_CUSTOM_GETTER(scriptGetSourceMapURL, (JSGlobalObject * globalObject, return JSValue::encode(jsString(vm, url)); } +JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedData, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue thisValue = JSValue::decode(thisValueEncoded); + auto* script = jsDynamicCast(thisValue); + if (UNLIKELY(!script)) { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + if (auto* buffer = script->getBytecodeBuffer()) { + return JSValue::encode(buffer); + } + + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataProduced, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue thisValue = JSValue::decode(thisValueEncoded); + auto* script = jsDynamicCast(thisValue); + if (UNLIKELY(!script)) { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + return JSValue::encode(jsBoolean(script->cachedDataProduced())); +} + +JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataRejected, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValueEncoded, PropertyName)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue thisValue = JSValue::decode(thisValueEncoded); + auto* script = jsDynamicCast(thisValue); + if (UNLIKELY(!script)) { + return ERR::INVALID_ARG_VALUE(scope, globalObject, "this"_s, thisValue, "must be a Script"_s); + } + + switch (script->cachedDataRejected()) { + case TriState::True: + return JSValue::encode(jsBoolean(true)); + case TriState::False: + return JSValue::encode(jsBoolean(false)); + default: + return JSValue::encode(jsUndefined()); + } +} + JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); @@ -1281,12 +1403,14 @@ private: STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMScriptPrototype, NodeVMScriptPrototype::Base); static const struct HashTableValue scriptPrototypeTableValues[] = { - { "cachedDataRejected"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedDataRejected, nullptr } }, { "createCachedData"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptCreateCachedData, 1 } }, { "runInContext"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInContext, 2 } }, { "runInNewContext"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInNewContext, 2 } }, { "runInThisContext"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInThisContext, 2 } }, { "sourceMapURL"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetSourceMapURL, nullptr } }, + { "cachedData"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedData, nullptr } }, + { "cachedDataProduced"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedDataProduced, nullptr } }, + { "cachedDataRejected"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedDataRejected, nullptr } }, }; // NodeVMGlobalObject* NodeVMGlobalObject::create(JSC::VM& vm, JSC::Structure* structure) @@ -1317,6 +1441,44 @@ const ClassInfo NodeVMScript::s_info = { "Script"_s, &Base::s_info, nullptr, nul const ClassInfo NodeVMScriptConstructor::s_info = { "Script"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMScriptConstructor) }; const ClassInfo NodeVMGlobalObject::s_info = { "NodeVMGlobalObject"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMGlobalObject) }; +JSC::ProgramExecutable* NodeVMScript::createExecutable() +{ + auto& vm = JSC::getVM(globalObject()); + m_cachedExecutable.set(vm, this, JSC::ProgramExecutable::create(globalObject(), m_source)); + return m_cachedExecutable.get(); +} + +void NodeVMScript::cacheBytecode() +{ + if (!m_cachedExecutable) { + createExecutable(); + } + + m_cachedBytecode = getBytecode(globalObject(), m_cachedExecutable.get(), m_source); + m_cachedDataProduced = m_cachedBytecode != nullptr; +} + +JSC::JSUint8Array* NodeVMScript::getBytecodeBuffer() +{ + if (!m_options.produceCachedData) { + return nullptr; + } + + if (!m_cachedBytecodeBuffer) { + if (!m_cachedBytecode) { + cacheBytecode(); + } + + ASSERT(m_cachedBytecode); + + std::span bytes = m_cachedBytecode->span(); + m_cachedBytecodeBuffer.set(vm(), this, WebCore::createBuffer(globalObject(), bytes)); + } + + ASSERT(m_cachedBytecodeBuffer); + return m_cachedBytecodeBuffer.get(); +} + DEFINE_VISIT_CHILDREN(NodeVMScript); template @@ -1325,7 +1487,8 @@ void NodeVMScript::visitChildrenImpl(JSCell* cell, Visitor& visitor) NodeVMScript* thisObject = jsCast(cell); ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); - visitor.append(thisObject->m_cachedDirectExecutable); + visitor.append(thisObject->m_cachedExecutable); + visitor.append(thisObject->m_cachedBytecodeBuffer); } NodeVMScriptConstructor::NodeVMScriptConstructor(VM& vm, Structure* structure) @@ -1359,9 +1522,9 @@ JSObject* NodeVMScript::createPrototype(VM& vm, JSGlobalObject* globalObject) return NodeVMScriptPrototype::create(vm, globalObject, NodeVMScriptPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); } -NodeVMScript* NodeVMScript::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, SourceCode source) +NodeVMScript* NodeVMScript::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, SourceCode source, ScriptOptions options) { - NodeVMScript* ptr = new (NotNull, allocateCell(vm)) NodeVMScript(vm, structure, source); + NodeVMScript* ptr = new (NotNull, allocateCell(vm)) NodeVMScript(vm, structure, source, WTFMove(options)); ptr->finishCreation(vm); return ptr; } @@ -1608,4 +1771,43 @@ static String stringifyAnonymousFunction(JSGlobalObject* globalObject, const Arg return program; } +static RefPtr getBytecode(JSGlobalObject* globalObject, JSC::ProgramExecutable* executable, JSC::SourceCode source) +{ + auto& vm = JSC::getVM(globalObject); + JSC::CodeCache* cache = vm.codeCache(); + JSC::ParserError parserError; + JSC::UnlinkedProgramCodeBlock* unlinked = cache->getUnlinkedProgramCodeBlock(vm, executable, source, {}, parserError); + if (!unlinked || parserError.isValid()) { + return nullptr; + } + JSC::LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? TaintedByWithScopeLexicallyScopedFeature : NoLexicallyScopedFeatures; + JSC::BytecodeCacheError bytecodeCacheError; + FileSystem::FileHandle fileHandle; + return JSC::serializeBytecode(vm, unlinked, source, JSC::SourceCodeType::ProgramType, lexicallyScopedFeatures, JSParserScriptMode::Classic, fileHandle, bytecodeCacheError, {}); +} + +static JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, JSC::SourceCode source) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSC::ProgramExecutable* executable = JSC::ProgramExecutable::create(globalObject, source); + RETURN_IF_EXCEPTION(scope, {}); + + RefPtr bytecode = getBytecode(globalObject, executable, source); + RETURN_IF_EXCEPTION(scope, {}); + + if (UNLIKELY(!bytecode)) { + return throwVMError(globalObject, scope, "createCachedData failed"_s); + } + + std::span bytes = bytecode->span(); + auto* buffer = WebCore::createBuffer(globalObject, bytes); + + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(buffer); + + return JSValue::encode(buffer); +} + } // namespace Bun diff --git a/test/js/node/test/parallel/test-vm-cached-data.js b/test/js/node/test/parallel/test-vm-cached-data.js new file mode 100644 index 0000000000..39ae3fcdac --- /dev/null +++ b/test/js/node/test/parallel/test-vm-cached-data.js @@ -0,0 +1,96 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const spawnSync = require('child_process').spawnSync; + +function getSource(tag) { + return `(function ${tag}() { return '${tag}'; })`; +} + +function produce(source, count) { + count ||= 1; + + const out = spawnSync(process.execPath, [ '-e', ` + 'use strict'; + const assert = require('assert'); + const vm = require('vm'); + + var data; + for (var i = 0; i < ${count}; i++) { + var script = new vm.Script(process.argv[1], { + produceCachedData: true + }); + + assert(!script.cachedDataProduced || script.cachedData instanceof Buffer); + + if (script.cachedDataProduced) + data = script.cachedData.toString('base64'); + } + console.log(data); + `, source]); + + assert.strictEqual(out.status, 0, String(out.stderr)); + + return Buffer.from(out.stdout.toString(), 'base64'); +} + +function testProduceConsume() { + const source = getSource('original'); + + const data = produce(source); + + for (const cachedData of common.getArrayBufferViews(data)) { + // It should consume code cache + const script = new vm.Script(source, { + cachedData + }); + assert(!script.cachedDataRejected); + assert.strictEqual(script.runInThisContext()(), 'original'); + } +} +testProduceConsume(); + +function testProduceMultiple() { + const source = getSource('original'); + + produce(source, 3); +} +testProduceMultiple(); + +function testRejectInvalid() { + const source = getSource('invalid'); + + const data = produce(source); + + // It should reject invalid code cache + const script = new vm.Script(getSource('invalid_1'), { + cachedData: data + }); + assert(script.cachedDataRejected); + assert.strictEqual(script.runInThisContext()(), 'invalid_1'); +} +testRejectInvalid(); + +function testRejectSlice() { + const source = getSource('slice'); + + const data = produce(source).slice(4); + + const script = new vm.Script(source, { + cachedData: data + }); + assert(script.cachedDataRejected); +} +testRejectSlice(); + +// It should throw on non-Buffer cachedData +assert.throws(() => { + new vm.Script('function abc() {}', { + cachedData: 'ohai' + }); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /must be an instance of Buffer, TypedArray, or DataView/ +}); diff --git a/test/js/node/test/parallel/test-vm-createcacheddata.js b/test/js/node/test/parallel/test-vm-createcacheddata.js new file mode 100644 index 0000000000..ba4d299b9c --- /dev/null +++ b/test/js/node/test/parallel/test-vm-createcacheddata.js @@ -0,0 +1,22 @@ +'use strict'; + +require('../common'); + +const { Script } = require('vm'); +const assert = require('assert'); + +const source = 'function x() {} const y = x();'; + +const script = new Script(source); +let cachedData = script.createCachedData(); +assert(cachedData instanceof Buffer); + +assert(!new Script(source, { cachedData }).cachedDataRejected); + +script.runInNewContext(); + +for (let i = 0; i < 10; i += 1) { + cachedData = script.createCachedData(); + + assert(!new Script(source, { cachedData }).cachedDataRejected); +} diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index 726dbde737..3850870d58 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -687,3 +687,51 @@ resp.text().then((a) => { delete URL.prototype.ok; } }); + +test("can't use export syntax in vm.Script", () => { + expect(() => { + const script = new Script("export default {};"); + script.runInThisContext(); + }).toThrow({ name: "SyntaxError", message: "Unexpected keyword 'export'" }); + + expect(() => { + const script = new Script("export default {};"); + script.createCachedData(); + }).toThrow({ message: "createCachedData failed" }); +}); + +test("rejects invalid bytecode", () => { + const cachedData = Buffer.from("fhqwhgads"); + const script = new Script("1 + 1;", { + cachedData, + }); + expect(script.cachedDataRejected).toBeTrue(); + expect(script.runInThisContext()).toBe(2); +}); + +test("accepts valid bytecode", () => { + const source = "1 + 1;"; + const firstScript = new Script(source, { + produceCachedData: false, + }); + const cachedData = firstScript.createCachedData(); + expect(cachedData).toBeDefined(); + expect(cachedData).toBeInstanceOf(Buffer); + const secondScript = new Script(source, { + cachedData, + }); + expect(secondScript.cachedDataRejected).toBeFalse(); + expect(firstScript.runInThisContext()).toBe(2); + expect(secondScript.runInThisContext()).toBe(2); +}); + +test("can't use bytecode from a different script", () => { + const firstScript = new Script("1 + 1;"); + const cachedData = firstScript.createCachedData(); + const secondScript = new Script("2 + 2;", { + cachedData, + }); + expect(secondScript.cachedDataRejected).toBeTrue(); + expect(firstScript.runInThisContext()).toBe(2); + expect(secondScript.runInThisContext()).toBe(4); +});