node:vm Script cachedData support (#19379)

This commit is contained in:
Kai Tamkun
2025-05-01 16:37:02 -07:00
committed by GitHub
parent dbd30fe33a
commit 992effaa26
6 changed files with 710 additions and 338 deletions

View File

@@ -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/ \;

View File

@@ -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)

View File

@@ -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<JSC::CachedBytecode> 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.<anonymous>"_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<int32_t>().min(), std::numeric_limits<int32_t>().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<int32_t>().min(), std::numeric_limits<int32_t>().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<uint8_t>& 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<JSC::JSArrayBufferView*>(cachedDataOpt)) {
if (!arrayBufferView->isDetached()) {
std::span<const uint8_t> span = arrayBufferView->span();
outCachedData = { span.begin(), span.end() };
isValidType = true;
}
} else if (auto* arrayBuffer = JSC::jsDynamicCast<JSC::JSArrayBuffer*>(cachedDataOpt); arrayBuffer && arrayBuffer->impl()) {
std::span<const uint8_t> 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<int64_t>& 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<int64_t>().max()), &timeoutValue);
RETURN_IF_EXCEPTION(scope, {});
outTimeout = timeoutValue;
return true;
}
return false;
}
};
class ScriptOptions : public BaseOptions {
public:
std::optional<int64_t> timeout = std::nullopt;
bool produceCachedData = false;
std::vector<uint8_t> 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<int64_t> 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<uint8_t> 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<NodeVMGlobalObject*>(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<JSArray*>(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<typename, JSC::SubspaceAccess mode> 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<uint8_t>& cachedData() { return m_options.cachedData; }
RefPtr<JSC::CachedBytecode> 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<JSC::DirectEvalExecutable> m_cachedDirectExecutable;
private:
JSC::SourceCode m_source;
RefPtr<JSC::CachedBytecode> m_cachedBytecode;
mutable JSC::WriteBarrier<JSC::JSUint8Array> m_cachedBytecodeBuffer;
mutable JSC::WriteBarrier<JSC::ProgramExecutable> 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.<anonymous>"_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<int32_t>().min(), std::numeric_limits<int32_t>().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<int32_t>().min(), std::numeric_limits<int32_t>().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<JSC::JSArrayBufferView>() || cell->inherits<JSC::JSArrayBuffer>()) {
isValidType = true;
} else if (JSC::JSArrayBufferView* view = JSC::jsDynamicCast<JSC::JSArrayBufferView*>(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<int64_t>* 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<int64_t>().max()), &timeoutValue);
RETURN_IF_EXCEPTION(scope, {});
*outTimeout = timeoutValue;
return true;
}
return false;
}
};
class ScriptOptions : public BaseOptions {
public:
bool importModuleDynamically = false;
std::optional<int64_t> 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<int64_t> 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<NodeVMGlobalObject*>(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<JSArray*>(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<uint8_t>& 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<JSC::CachedBytecode> cachedBytecode = JSC::CachedBytecode::create(std::span(cachedData), nullptr, {});
JSC::UnlinkedProgramCodeBlock* unlinkedBlock = JSC::decodeCodeBlock<UnlinkedProgramCodeBlock>(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> 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<NodeVMScript*>(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<NodeVMScript*>(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<NodeVMScript*>(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<NodeVMScript*>(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<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedDataRejected, nullptr } },
{ "createCachedData"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptCreateCachedData, 1 } },
{ "runInContext"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInContext, 2 } },
{ "runInNewContext"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInNewContext, 2 } },
{ "runInThisContext"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInThisContext, 2 } },
{ "sourceMapURL"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetSourceMapURL, nullptr } },
{ "cachedData"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedData, nullptr } },
{ "cachedDataProduced"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedDataProduced, nullptr } },
{ "cachedDataRejected"_s, static_cast<unsigned>(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<const uint8_t> 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<typename Visitor>
@@ -1325,7 +1487,8 @@ void NodeVMScript::visitChildrenImpl(JSCell* cell, Visitor& visitor)
NodeVMScript* thisObject = jsCast<NodeVMScript*>(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<NodeVMScript>(vm)) NodeVMScript(vm, structure, source);
NodeVMScript* ptr = new (NotNull, allocateCell<NodeVMScript>(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<JSC::CachedBytecode> 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<JSC::CachedBytecode> bytecode = getBytecode(globalObject, executable, source);
RETURN_IF_EXCEPTION(scope, {});
if (UNLIKELY(!bytecode)) {
return throwVMError(globalObject, scope, "createCachedData failed"_s);
}
std::span<const uint8_t> bytes = bytecode->span();
auto* buffer = WebCore::createBuffer(globalObject, bytes);
RETURN_IF_EXCEPTION(scope, {});
ASSERT(buffer);
return JSValue::encode(buffer);
}
} // namespace Bun

View File

@@ -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/
});

View File

@@ -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);
}

View File

@@ -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);
});