From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- js/src/shell/Makefile.in | 16 + js/src/shell/ModuleLoader.cpp | 672 ++ js/src/shell/ModuleLoader.h | 94 + js/src/shell/OSObject.cpp | 1309 +++ js/src/shell/OSObject.h | 99 + js/src/shell/ShellModuleObjectWrapper.cpp | 469 + js/src/shell/ShellModuleObjectWrapper.h | 36 + js/src/shell/StringUtils.h | 146 + js/src/shell/WasmTesting.cpp | 64 + js/src/shell/WasmTesting.h | 37 + js/src/shell/fuzz-flags.txt | 97 + js/src/shell/js-gdb.py | 21 + js/src/shell/js.cpp | 12594 ++++++++++++++++++++++ js/src/shell/jsoptparse.cpp | 639 ++ js/src/shell/jsoptparse.h | 336 + js/src/shell/jsrtfuzzing/jsrtfuzzing-example.js | 42 + js/src/shell/jsrtfuzzing/jsrtfuzzing.cpp | 139 + js/src/shell/jsrtfuzzing/jsrtfuzzing.h | 28 + js/src/shell/jsshell.cpp | 128 + js/src/shell/jsshell.h | 263 + js/src/shell/moz.build | 59 + 21 files changed, 17288 insertions(+) create mode 100644 js/src/shell/Makefile.in create mode 100644 js/src/shell/ModuleLoader.cpp create mode 100644 js/src/shell/ModuleLoader.h create mode 100644 js/src/shell/OSObject.cpp create mode 100644 js/src/shell/OSObject.h create mode 100644 js/src/shell/ShellModuleObjectWrapper.cpp create mode 100644 js/src/shell/ShellModuleObjectWrapper.h create mode 100644 js/src/shell/StringUtils.h create mode 100644 js/src/shell/WasmTesting.cpp create mode 100644 js/src/shell/WasmTesting.h create mode 100644 js/src/shell/fuzz-flags.txt create mode 100644 js/src/shell/js-gdb.py create mode 100644 js/src/shell/js.cpp create mode 100644 js/src/shell/jsoptparse.cpp create mode 100644 js/src/shell/jsoptparse.h create mode 100644 js/src/shell/jsrtfuzzing/jsrtfuzzing-example.js create mode 100644 js/src/shell/jsrtfuzzing/jsrtfuzzing.cpp create mode 100644 js/src/shell/jsrtfuzzing/jsrtfuzzing.h create mode 100644 js/src/shell/jsshell.cpp create mode 100644 js/src/shell/jsshell.h create mode 100644 js/src/shell/moz.build (limited to 'js/src/shell') diff --git a/js/src/shell/Makefile.in b/js/src/shell/Makefile.in new file mode 100644 index 0000000000..3ce017a08d --- /dev/null +++ b/js/src/shell/Makefile.in @@ -0,0 +1,16 @@ +# -*- Mode: makefile -*- +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +ifdef QEMU_EXE +MOZ_POST_PROGRAM_COMMAND = $(topsrcdir)/build/qemu-wrap --qemu $(QEMU_EXE) --libdir $(CROSS_LIB) +endif + +include $(topsrcdir)/config/rules.mk + +# Install versioned binary for parallel installability in Linux distributions +install:: $(PROGRAM) + cp $^ $(basename $^)$(MOZJS_MAJOR_VERSION)$(suffix $^) + $(SYSINSTALL) $(basename $^)$(MOZJS_MAJOR_VERSION)$(suffix $^) $(DESTDIR)$(bindir) diff --git a/js/src/shell/ModuleLoader.cpp b/js/src/shell/ModuleLoader.cpp new file mode 100644 index 0000000000..374fdee6cf --- /dev/null +++ b/js/src/shell/ModuleLoader.cpp @@ -0,0 +1,672 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 + * -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "shell/ModuleLoader.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/TextUtils.h" + +#include "jsapi.h" +#include "NamespaceImports.h" + +#include "builtin/TestingUtility.h" // js::CreateScriptPrivate +#include "js/Conversions.h" +#include "js/MapAndSet.h" +#include "js/Modules.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetProperty +#include "js/SourceText.h" +#include "js/StableStringChars.h" +#include "shell/jsshell.h" +#include "shell/OSObject.h" +#include "shell/StringUtils.h" +#include "util/Text.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/StringType.h" + +using namespace js; +using namespace js::shell; + +static constexpr char16_t JavaScriptScheme[] = u"javascript:"; + +static bool IsJavaScriptURL(Handle path) { + return StringStartsWith(path, JavaScriptScheme); +} + +static JSString* ExtractJavaScriptURLSource(JSContext* cx, + Handle path) { + MOZ_ASSERT(IsJavaScriptURL(path)); + + const size_t schemeLength = js_strlen(JavaScriptScheme); + return SubString(cx, path, schemeLength); +} + +bool ModuleLoader::init(JSContext* cx, HandleString loadPath) { + loadPathStr = AtomizeString(cx, loadPath); + if (!loadPathStr || !PinAtom(cx, loadPathStr)) { + return false; + } + + MOZ_ASSERT(IsAbsolutePath(loadPathStr)); + + char16_t sep = PathSeparator; + pathSeparatorStr = AtomizeChars(cx, &sep, 1); + if (!pathSeparatorStr || !PinAtom(cx, pathSeparatorStr)) { + return false; + } + + JSRuntime* rt = cx->runtime(); + JS::SetModuleResolveHook(rt, ModuleLoader::ResolveImportedModule); + JS::SetModuleMetadataHook(rt, ModuleLoader::GetImportMetaProperties); + JS::SetModuleDynamicImportHook(rt, ModuleLoader::ImportModuleDynamically); + + JS::ImportAssertionVector assertions; + MOZ_ALWAYS_TRUE(assertions.reserve(1)); + assertions.infallibleAppend(JS::ImportAssertion::Type); + JS::SetSupportedImportAssertions(rt, assertions); + + return true; +} + +// static +JSObject* ModuleLoader::ResolveImportedModule( + JSContext* cx, JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest) { + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->resolveImportedModule(cx, referencingPrivate, + moduleRequest); +} + +// static +bool ModuleLoader::GetImportMetaProperties(JSContext* cx, + JS::HandleValue privateValue, + JS::HandleObject metaObject) { + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->populateImportMeta(cx, privateValue, metaObject); +} + +bool ModuleLoader::ImportMetaResolve(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedValue modulePrivate( + cx, js::GetFunctionNativeReserved(&args.callee(), ModulePrivateSlot)); + + // https://html.spec.whatwg.org/#hostgetimportmetaproperties + // Step 4.1. Set specifier to ? ToString(specifier). + // + // https://tc39.es/ecma262/#sec-tostring + RootedValue v(cx, args.get(ImportMetaResolveSpecifierArg)); + RootedString specifier(cx, JS::ToString(cx, v)); + if (!specifier) { + return false; + } + + // Step 4.2, 4.3 are implemented in importMetaResolve. + ShellContext* scx = GetShellContext(cx); + RootedString url(cx); + if (!scx->moduleLoader->importMetaResolve(cx, modulePrivate, specifier, + &url)) { + return false; + } + + // Step 4.4. Return the serialization of url. + args.rval().setString(url); + return true; +} + +// static +bool ModuleLoader::ImportModuleDynamically(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest, + JS::HandleObject promise) { + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->dynamicImport(cx, referencingPrivate, moduleRequest, + promise); +} + +// static +bool ModuleLoader::GetSupportedImportAssertions( + JSContext* cx, JS::ImportAssertionVector& values) { + MOZ_ASSERT(values.empty()); + + if (!values.reserve(1)) { + ReportOutOfMemory(cx); + return false; + } + + values.infallibleAppend(JS::ImportAssertion::Type); + + return true; +} + +bool ModuleLoader::loadRootModule(JSContext* cx, HandleString path) { + RootedValue rval(cx); + if (!loadAndExecute(cx, path, &rval)) { + return false; + } + + RootedObject evaluationPromise(cx, &rval.toObject()); + if (evaluationPromise == nullptr) { + return false; + } + + return JS::ThrowOnModuleEvaluationFailure(cx, evaluationPromise); +} + +bool ModuleLoader::registerTestModule(JSContext* cx, HandleObject moduleRequest, + Handle module) { + Rooted path( + cx, resolve(cx, moduleRequest, UndefinedHandleValue)); + if (!path) { + return false; + } + + path = normalizePath(cx, path); + if (!path) { + return false; + } + + return addModuleToRegistry(cx, path, module); +} + +void ModuleLoader::clearModules(JSContext* cx) { + Handle global = cx->global(); + global->setReservedSlot(GlobalAppSlotModuleRegistry, UndefinedValue()); +} + +bool ModuleLoader::loadAndExecute(JSContext* cx, HandleString path, + MutableHandleValue rval) { + RootedObject module(cx, loadAndParse(cx, path)); + if (!module) { + return false; + } + + if (!JS::ModuleLink(cx, module)) { + return false; + } + + return JS::ModuleEvaluate(cx, module, rval); +} + +JSObject* ModuleLoader::resolveImportedModule( + JSContext* cx, JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest) { + Rooted path(cx, + resolve(cx, moduleRequest, referencingPrivate)); + if (!path) { + return nullptr; + } + + return loadAndParse(cx, path); +} + +bool ModuleLoader::populateImportMeta(JSContext* cx, + JS::HandleValue privateValue, + JS::HandleObject metaObject) { + Rooted path(cx); + if (!privateValue.isUndefined()) { + if (!getScriptPath(cx, privateValue, &path)) { + return false; + } + } + + if (!path) { + path = NewStringCopyZ(cx, "(unknown)"); + if (!path) { + return false; + } + } + + RootedValue pathValue(cx, StringValue(path)); + if (!JS_DefineProperty(cx, metaObject, "url", pathValue, JSPROP_ENUMERATE)) { + return false; + } + + JSFunction* resolveFunc = js::DefineFunctionWithReserved( + cx, metaObject, "resolve", ImportMetaResolve, ImportMetaResolveNumArgs, + JSPROP_ENUMERATE); + if (!resolveFunc) { + return false; + } + + RootedObject resolveFuncObj(cx, JS_GetFunctionObject(resolveFunc)); + js::SetFunctionNativeReserved(resolveFuncObj, ModulePrivateSlot, + privateValue); + + return true; +} + +bool ModuleLoader::importMetaResolve(JSContext* cx, + JS::Handle referencingPrivate, + JS::Handle specifier, + JS::MutableHandle urlOut) { + Rooted path(cx, resolve(cx, specifier, referencingPrivate)); + if (!path) { + return false; + } + + urlOut.set(path); + return true; +} + +bool ModuleLoader::dynamicImport(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest, + JS::HandleObject promise) { + // To make this more realistic, use a promise to delay the import and make it + // happen asynchronously. This method packages up the arguments and creates a + // resolved promise, which on fullfillment calls doDynamicImport with the + // original arguments. + + MOZ_ASSERT(promise); + RootedValue moduleRequestValue(cx, ObjectValue(*moduleRequest)); + RootedValue promiseValue(cx, ObjectValue(*promise)); + RootedObject closure(cx, JS_NewObjectWithGivenProto(cx, nullptr, nullptr)); + if (!closure || + !JS_DefineProperty(cx, closure, "referencingPrivate", referencingPrivate, + JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, closure, "moduleRequest", moduleRequestValue, + JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, closure, "promise", promiseValue, + JSPROP_ENUMERATE)) { + return false; + } + + RootedFunction onResolved( + cx, NewNativeFunction(cx, DynamicImportDelayFulfilled, 1, nullptr)); + if (!onResolved) { + return false; + } + + RootedFunction onRejected( + cx, NewNativeFunction(cx, DynamicImportDelayRejected, 1, nullptr)); + if (!onRejected) { + return false; + } + + RootedObject delayPromise(cx); + RootedValue closureValue(cx, ObjectValue(*closure)); + delayPromise = PromiseObject::unforgeableResolve(cx, closureValue); + if (!delayPromise) { + return false; + } + + return JS::AddPromiseReactions(cx, delayPromise, onResolved, onRejected); +} + +bool ModuleLoader::DynamicImportDelayFulfilled(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject closure(cx, &args[0].toObject()); + + RootedValue referencingPrivate(cx); + RootedValue moduleRequestValue(cx); + RootedValue promiseValue(cx); + if (!JS_GetProperty(cx, closure, "referencingPrivate", &referencingPrivate) || + !JS_GetProperty(cx, closure, "moduleRequest", &moduleRequestValue) || + !JS_GetProperty(cx, closure, "promise", &promiseValue)) { + return false; + } + + RootedObject moduleRequest(cx, &moduleRequestValue.toObject()); + RootedObject promise(cx, &promiseValue.toObject()); + + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->doDynamicImport(cx, referencingPrivate, + moduleRequest, promise); +} + +bool ModuleLoader::DynamicImportDelayRejected(JSContext* cx, unsigned argc, + Value* vp) { + MOZ_CRASH("This promise should never be rejected"); +} + +bool ModuleLoader::doDynamicImport(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest, + JS::HandleObject promise) { + // Exceptions during dynamic import are handled by calling + // FinishDynamicModuleImport with a pending exception on the context. + RootedValue rval(cx); + bool ok = + tryDynamicImport(cx, referencingPrivate, moduleRequest, promise, &rval); + JSObject* evaluationObject = ok ? &rval.toObject() : nullptr; + RootedObject evaluationPromise(cx, evaluationObject); + return JS::FinishDynamicModuleImport( + cx, evaluationPromise, referencingPrivate, moduleRequest, promise); +} + +bool ModuleLoader::tryDynamicImport(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest, + JS::HandleObject promise, + JS::MutableHandleValue rval) { + Rooted path(cx, + resolve(cx, moduleRequest, referencingPrivate)); + if (!path) { + return false; + } + + return loadAndExecute(cx, path, rval); +} + +JSLinearString* ModuleLoader::resolve(JSContext* cx, + HandleObject moduleRequestArg, + HandleValue referencingInfo) { + ModuleRequestObject* moduleRequest = + &moduleRequestArg->as(); + if (moduleRequest->specifier()->length() == 0) { + JS_ReportErrorASCII(cx, "Invalid module specifier"); + return nullptr; + } + + Rooted name( + cx, JS_EnsureLinearString(cx, moduleRequest->specifier())); + if (!name) { + return nullptr; + } + + return resolve(cx, name, referencingInfo); +} + +JSLinearString* ModuleLoader::resolve(JSContext* cx, HandleString specifier, + HandleValue referencingInfo) { + Rooted name(cx, JS_EnsureLinearString(cx, specifier)); + if (!name) { + return nullptr; + } + + if (IsJavaScriptURL(name) || IsAbsolutePath(name)) { + return name; + } + + // Treat |name| as a relative path if it starts with either "./" or "../". + bool isRelative = + StringStartsWith(name, u"./") || StringStartsWith(name, u"../") +#ifdef XP_WIN + || StringStartsWith(name, u".\\") || StringStartsWith(name, u"..\\") +#endif + ; + + RootedString path(cx, loadPathStr); + + if (isRelative) { + if (referencingInfo.isUndefined()) { + JS_ReportErrorASCII(cx, "No referencing module for relative import"); + return nullptr; + } + + Rooted refPath(cx); + if (!getScriptPath(cx, referencingInfo, &refPath)) { + return nullptr; + } + + if (!refPath) { + JS_ReportErrorASCII(cx, "No path set for referencing module"); + return nullptr; + } + + int32_t sepIndex = LastIndexOf(refPath, u'/'); +#ifdef XP_WIN + sepIndex = std::max(sepIndex, LastIndexOf(refPath, u'\\')); +#endif + if (sepIndex >= 0) { + path = SubString(cx, refPath, 0, sepIndex); + if (!path) { + return nullptr; + } + } + } + + RootedString result(cx); + RootedString pathSep(cx, pathSeparatorStr); + result = JS_ConcatStrings(cx, path, pathSep); + if (!result) { + return nullptr; + } + + result = JS_ConcatStrings(cx, result, name); + if (!result) { + return nullptr; + } + + Rooted linear(cx, JS_EnsureLinearString(cx, result)); + if (!linear) { + return nullptr; + } + return normalizePath(cx, linear); +} + +JSObject* ModuleLoader::loadAndParse(JSContext* cx, HandleString pathArg) { + Rooted path(cx, JS_EnsureLinearString(cx, pathArg)); + if (!path) { + return nullptr; + } + + path = normalizePath(cx, path); + if (!path) { + return nullptr; + } + + RootedObject module(cx); + if (!lookupModuleInRegistry(cx, path, &module)) { + return nullptr; + } + + if (module) { + return module; + } + + UniqueChars filename = JS_EncodeStringToUTF8(cx, path); + if (!filename) { + return nullptr; + } + + JS::CompileOptions options(cx); + options.setFileAndLine(filename.get(), 1); + + RootedString source(cx, fetchSource(cx, path)); + if (!source) { + return nullptr; + } + + JS::AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, source)) { + return nullptr; + } + + JS::SourceText srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return nullptr; + } + + module = JS::CompileModule(cx, options, srcBuf); + if (!module) { + return nullptr; + } + + RootedObject info(cx, js::CreateScriptPrivate(cx, path)); + if (!info) { + return nullptr; + } + + JS::SetModulePrivate(module, ObjectValue(*info)); + + if (!addModuleToRegistry(cx, path, module)) { + return nullptr; + } + + return module; +} + +bool ModuleLoader::lookupModuleInRegistry(JSContext* cx, HandleString path, + MutableHandleObject moduleOut) { + moduleOut.set(nullptr); + + RootedObject registry(cx, getOrCreateModuleRegistry(cx)); + if (!registry) { + return false; + } + + RootedValue pathValue(cx, StringValue(path)); + RootedValue moduleValue(cx); + if (!JS::MapGet(cx, registry, pathValue, &moduleValue)) { + return false; + } + + if (!moduleValue.isUndefined()) { + moduleOut.set(&moduleValue.toObject()); + } + + return true; +} + +bool ModuleLoader::addModuleToRegistry(JSContext* cx, HandleString path, + HandleObject module) { + RootedObject registry(cx, getOrCreateModuleRegistry(cx)); + if (!registry) { + return false; + } + + RootedValue pathValue(cx, StringValue(path)); + RootedValue moduleValue(cx, ObjectValue(*module)); + return JS::MapSet(cx, registry, pathValue, moduleValue); +} + +JSObject* ModuleLoader::getOrCreateModuleRegistry(JSContext* cx) { + Handle global = cx->global(); + RootedValue value(cx, global->getReservedSlot(GlobalAppSlotModuleRegistry)); + if (!value.isUndefined()) { + return &value.toObject(); + } + + JSObject* registry = JS::NewMapObject(cx); + if (!registry) { + return nullptr; + } + + global->setReservedSlot(GlobalAppSlotModuleRegistry, ObjectValue(*registry)); + return registry; +} + +bool ModuleLoader::getScriptPath(JSContext* cx, HandleValue privateValue, + MutableHandle pathOut) { + pathOut.set(nullptr); + + RootedObject infoObj(cx, &privateValue.toObject()); + RootedValue pathValue(cx); + if (!JS_GetProperty(cx, infoObj, "path", &pathValue)) { + return false; + } + + if (pathValue.isUndefined()) { + return true; + } + + RootedString path(cx, pathValue.toString()); + pathOut.set(JS_EnsureLinearString(cx, path)); + return pathOut; +} + +JSLinearString* ModuleLoader::normalizePath(JSContext* cx, + Handle pathArg) { + Rooted path(cx, pathArg); + + if (IsJavaScriptURL(path)) { + return path; + } + +#ifdef XP_WIN + // Replace all forward slashes with backward slashes. + path = ReplaceCharGlobally(cx, path, u'/', PathSeparator); + if (!path) { + return nullptr; + } + + // Remove the drive letter, if present. + Rooted drive(cx); + if (path->length() > 2 && mozilla::IsAsciiAlpha(CharAt(path, 0)) && + CharAt(path, 1) == u':' && CharAt(path, 2) == u'\\') { + drive = SubString(cx, path, 0, 2); + path = SubString(cx, path, 2); + if (!drive || !path) { + return nullptr; + } + } +#endif // XP_WIN + + // Normalize the path by removing redundant path components. + Rooted> components(cx, cx); + size_t lastSep = 0; + while (lastSep < path->length()) { + int32_t i = IndexOf(path, PathSeparator, lastSep); + if (i < 0) { + i = path->length(); + } + + Rooted part(cx, SubString(cx, path, lastSep, i)); + if (!part) { + return nullptr; + } + + lastSep = i + 1; + + // Remove "." when preceded by a path component. + if (StringEquals(part, u".") && !components.empty()) { + continue; + } + + if (StringEquals(part, u"..") && !components.empty()) { + // Replace "./.." with "..". + if (StringEquals(components.back(), u".")) { + components.back() = part; + continue; + } + + // When preceded by a non-empty path component, remove ".." and the + // preceding component, unless the preceding component is also "..". + if (!StringEquals(components.back(), u"") && + !StringEquals(components.back(), u"..")) { + components.popBack(); + continue; + } + } + + if (!components.append(part)) { + return nullptr; + } + } + + Rooted pathSep(cx, pathSeparatorStr); + RootedString normalized(cx, JoinStrings(cx, components, pathSep)); + if (!normalized) { + return nullptr; + } + +#ifdef XP_WIN + if (drive) { + normalized = JS_ConcatStrings(cx, drive, normalized); + if (!normalized) { + return nullptr; + } + } +#endif + + return JS_EnsureLinearString(cx, normalized); +} + +JSString* ModuleLoader::fetchSource(JSContext* cx, + Handle path) { + if (IsJavaScriptURL(path)) { + return ExtractJavaScriptURLSource(cx, path); + } + + RootedString resolvedPath(cx, ResolvePath(cx, path, RootRelative)); + if (!resolvedPath) { + return nullptr; + } + + return FileAsString(cx, resolvedPath); +} diff --git a/js/src/shell/ModuleLoader.h b/js/src/shell/ModuleLoader.h new file mode 100644 index 0000000000..c8f0147356 --- /dev/null +++ b/js/src/shell/ModuleLoader.h @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef shell_ModuleLoader_h +#define shell_ModuleLoader_h + +#include "builtin/ModuleObject.h" +#include "js/RootingAPI.h" + +namespace js { +namespace shell { + +class ModuleLoader { + public: + bool init(JSContext* cx, HandleString loadPath); + bool loadRootModule(JSContext* cx, HandleString path); + + // Testing hook to register a module that wasn't loaded by the module loader. + bool registerTestModule(JSContext* cx, HandleObject moduleRequest, + Handle module); + + // Testing hook to clear all loaded modules. + void clearModules(JSContext* cx); + + private: + static JSObject* ResolveImportedModule(JSContext* cx, + HandleValue referencingPrivate, + HandleObject moduleRequest); + static bool GetImportMetaProperties(JSContext* cx, HandleValue privateValue, + HandleObject metaObject); + static bool ImportMetaResolve(JSContext* cx, unsigned argc, Value* vp); + static bool ImportModuleDynamically(JSContext* cx, + HandleValue referencingPrivate, + HandleObject moduleRequest, + HandleObject promise); + static bool GetSupportedImportAssertions(JSContext* cx, + JS::ImportAssertionVector& values); + + static bool DynamicImportDelayFulfilled(JSContext* cx, unsigned argc, + Value* vp); + static bool DynamicImportDelayRejected(JSContext* cx, unsigned argc, + Value* vp); + + bool loadAndExecute(JSContext* cx, HandleString path, MutableHandleValue); + JSObject* resolveImportedModule(JSContext* cx, HandleValue referencingPrivate, + HandleObject moduleRequest); + bool populateImportMeta(JSContext* cx, HandleValue privateValue, + HandleObject metaObject); + bool importMetaResolve(JSContext* cx, + JS::Handle referencingPrivate, + JS::Handle specifier, + JS::MutableHandle urlOut); + bool dynamicImport(JSContext* cx, HandleValue referencingPrivate, + HandleObject moduleRequest, HandleObject promise); + bool doDynamicImport(JSContext* cx, HandleValue referencingPrivate, + HandleObject moduleRequest, HandleObject promise); + bool tryDynamicImport(JSContext* cx, HandleValue referencingPrivate, + HandleObject moduleRequest, HandleObject promise, + MutableHandleValue rval); + JSObject* loadAndParse(JSContext* cx, HandleString path); + bool lookupModuleInRegistry(JSContext* cx, HandleString path, + MutableHandleObject moduleOut); + bool addModuleToRegistry(JSContext* cx, HandleString path, + HandleObject module); + JSLinearString* resolve(JSContext* cx, HandleObject moduleRequestArg, + HandleValue referencingInfo); + JSLinearString* resolve(JSContext* cx, HandleString specifier, + HandleValue referencingInfo); + bool getScriptPath(JSContext* cx, HandleValue privateValue, + MutableHandle pathOut); + JSLinearString* normalizePath(JSContext* cx, Handle path); + JSObject* getOrCreateModuleRegistry(JSContext* cx); + JSString* fetchSource(JSContext* cx, Handle path); + + // The following are used for pinned atoms which do not need rooting. + JSAtom* loadPathStr = nullptr; + JSAtom* pathSeparatorStr = nullptr; + + // The slot stored in ImportMetaResolve function. + enum { ModulePrivateSlot = 0, SlotCount }; + + // The number of args in ImportMetaResolve. + static const uint32_t ImportMetaResolveNumArgs = 1; + // The index of the 'specifier' argument in ImportMetaResolve. + static const uint32_t ImportMetaResolveSpecifierArg = 0; +} JS_HAZ_NON_GC_POINTER; + +} // namespace shell +} // namespace js + +#endif // shell_ModuleLoader_h diff --git a/js/src/shell/OSObject.cpp b/js/src/shell/OSObject.cpp new file mode 100644 index 0000000000..b618d331e0 --- /dev/null +++ b/js/src/shell/OSObject.cpp @@ -0,0 +1,1309 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// OSObject.h - os object for exposing posix system calls in the JS shell + +#include "shell/OSObject.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/TextUtils.h" + +#include +#include +#ifdef XP_WIN +# include +# include +# include +# include +# include +#elif __wasi__ +# include +# include +# include +#else +# include +# include +# include +# include +#endif + +#include "jsapi.h" +// For JSFunctionSpecWithHelp +#include "jsfriendapi.h" + +#include "gc/GCContext.h" +#include "js/CharacterEncoding.h" +#include "js/Conversions.h" +#include "js/experimental/TypedData.h" // JS_NewUint8Array +#include "js/Object.h" // JS::GetReservedSlot +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "js/PropertySpec.h" +#include "js/Value.h" // JS::Value +#include "js/Wrapper.h" +#include "shell/jsshell.h" +#include "shell/StringUtils.h" +#include "util/GetPidProvider.h" // getpid() +#include "util/StringBuffer.h" +#include "util/Text.h" +#include "util/WindowsWrapper.h" +#include "vm/JSObject.h" +#include "vm/TypedArrayObject.h" + +#include "vm/JSObject-inl.h" + +#ifdef XP_WIN +# ifndef PATH_MAX +# define PATH_MAX (MAX_PATH > _MAX_DIR ? MAX_PATH : _MAX_DIR) +# endif +# define getcwd _getcwd +#elif defined(__wasi__) +// Nothing. +#else +# include +#endif + +using js::shell::RCFile; + +namespace js { +namespace shell { + +bool IsAbsolutePath(JSLinearString* filename) { + size_t length = filename->length(); + +#ifdef XP_WIN + // On Windows there are various forms of absolute paths (see + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx + // for details): + // + // "\..." + // "\\..." + // "C:\..." + // + // The first two cases are handled by the common test below so we only need a + // specific test for the last one here. + + if (length > 3 && mozilla::IsAsciiAlpha(CharAt(filename, 0)) && + CharAt(filename, 1) == u':' && CharAt(filename, 2) == u'\\') { + return true; + } +#endif + + return length > 0 && CharAt(filename, 0) == PathSeparator; +} + +static UniqueChars DirectoryName(JSContext* cx, const char* path) { +#ifdef XP_WIN + UniqueWideChars widePath = JS::EncodeUtf8ToWide(cx, path); + if (!widePath) { + return nullptr; + } + + wchar_t dirName[PATH_MAX + 1]; + wchar_t* drive = nullptr; + wchar_t* fileName = nullptr; + wchar_t* fileExt = nullptr; + + // The docs say it can return EINVAL, but the compiler says it's void + _wsplitpath(widePath.get(), drive, dirName, fileName, fileExt); + + return JS::EncodeWideToUtf8(cx, dirName); +#else + UniqueChars narrowPath = JS::EncodeUtf8ToNarrow(cx, path); + if (!narrowPath) { + return nullptr; + } + + char dirName[PATH_MAX + 1]; + strncpy(dirName, narrowPath.get(), PATH_MAX); + if (dirName[PATH_MAX - 1] != '\0') { + return nullptr; + } + +# ifdef __wasi__ + // dirname() seems not to behave properly with wasi-libc; so we do our own + // simple thing here. + char* p = dirName + strlen(dirName); + bool found = false; + while (p > dirName) { + if (*p == '/') { + found = true; + *p = '\0'; + break; + } + p--; + } + if (!found) { + // There's no '/'. Possible cases are the following: + // * "." + // * ".." + // * filename only + // + // dirname() returns "." for all cases. + dirName[0] = '.'; + dirName[1] = '\0'; + } +# else + // dirname(dirName) might return dirName, or it might return a + // statically-allocated string + memmove(dirName, dirname(dirName), strlen(dirName) + 1); +# endif + + return JS::EncodeNarrowToUtf8(cx, dirName); +#endif +} + +/* + * Resolve a (possibly) relative filename to an absolute path. If + * |scriptRelative| is true, then the result will be relative to the directory + * containing the currently-running script, or the current working directory if + * the currently-running script is "-e" (namely, you're using it from the + * command line.) Otherwise, it will be relative to the current working + * directory. + */ +JSString* ResolvePath(JSContext* cx, HandleString filenameStr, + PathResolutionMode resolveMode) { + if (!filenameStr) { +#ifdef XP_WIN + return JS_NewStringCopyZ(cx, "nul"); +#elif defined(__wasi__) + MOZ_CRASH("NYI for WASI"); + return nullptr; +#else + return JS_NewStringCopyZ(cx, "/dev/null"); +#endif + } + + Rooted str(cx, JS_EnsureLinearString(cx, filenameStr)); + if (!str) { + return nullptr; + } + + if (IsAbsolutePath(str)) { + return str; + } + + UniqueChars filename = JS_EncodeStringToUTF8(cx, str); + if (!filename) { + return nullptr; + } + + JS::AutoFilename scriptFilename; + if (resolveMode == ScriptRelative) { + // Get the currently executing script's name. + if (!DescribeScriptedCaller(cx, &scriptFilename)) { + return nullptr; + } + + if (!scriptFilename.get()) { + return nullptr; + } + + if (strcmp(scriptFilename.get(), "-e") == 0 || + strcmp(scriptFilename.get(), "typein") == 0) { + resolveMode = RootRelative; + } + } + + UniqueChars path; + if (resolveMode == ScriptRelative) { + path = DirectoryName(cx, scriptFilename.get()); + } else { + path = GetCWD(cx); + } + + if (!path) { + return nullptr; + } + + size_t pathLen = strlen(path.get()); + size_t filenameLen = strlen(filename.get()); + size_t resultLen = pathLen + 1 + filenameLen; + + UniqueChars result = cx->make_pod_array(resultLen + 1); + if (!result) { + return nullptr; + } + memcpy(result.get(), path.get(), pathLen); + result[pathLen] = '/'; + memcpy(result.get() + pathLen + 1, filename.get(), filenameLen); + result[pathLen + 1 + filenameLen] = '\0'; + + return JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(result.get(), resultLen)); +} + +FILE* OpenFile(JSContext* cx, const char* filename, const char* mode) { +#ifdef XP_WIN + // Maximum valid mode string is "w+xb". Longer strings or strings + // containing invalid input lead to undefined behavior. + constexpr size_t MaxValidModeLength = 4; + wchar_t wideMode[MaxValidModeLength + 1] = {0}; + for (size_t i = 0; i < MaxValidModeLength && mode[i] != '\0'; i++) { + wideMode[i] = mode[i] & 0x7f; + } + + UniqueWideChars wideFilename = JS::EncodeUtf8ToWide(cx, filename); + if (!wideFilename) { + return nullptr; + } + + FILE* file = _wfopen(wideFilename.get(), wideMode); +#else + UniqueChars narrowFilename = JS::EncodeUtf8ToNarrow(cx, filename); + if (!narrowFilename) { + return nullptr; + } + + FILE* file = fopen(narrowFilename.get(), mode); +#endif + + if (!file) { + if (UniqueChars error = SystemErrorMessage(cx, errno)) { + JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr, + JSSMSG_CANT_OPEN, filename, error.get()); + } + return nullptr; + } + return file; +} + +bool ReadFile(JSContext* cx, const char* filename, FILE* file, char* buffer, + size_t length) { + size_t cc = fread(buffer, sizeof(char), length, file); + if (cc != length) { + if (ptrdiff_t(cc) < 0) { + if (UniqueChars error = SystemErrorMessage(cx, errno)) { + JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr, + JSSMSG_CANT_READ, filename, error.get()); + } + } else { + JS_ReportErrorUTF8(cx, "can't read %s: short read", filename); + } + return false; + } + return true; +} + +bool FileSize(JSContext* cx, const char* filename, FILE* file, size_t* size) { + if (fseek(file, 0, SEEK_END) != 0) { + JS_ReportErrorUTF8(cx, "can't seek end of %s", filename); + return false; + } + + size_t len = ftell(file); + if (fseek(file, 0, SEEK_SET) != 0) { + JS_ReportErrorUTF8(cx, "can't seek start of %s", filename); + return false; + } + + *size = len; + return true; +} + +JSObject* FileAsTypedArray(JSContext* cx, JS::HandleString pathnameStr) { + UniqueChars pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + + FILE* file = OpenFile(cx, pathname.get(), "rb"); + if (!file) { + return nullptr; + } + AutoCloseFile autoClose(file); + + size_t len; + if (!FileSize(cx, pathname.get(), file, &len)) { + return nullptr; + } + + if (len > ArrayBufferObject::MaxByteLength) { + JS_ReportErrorUTF8(cx, "file %s is too large for a Uint8Array", + pathname.get()); + return nullptr; + } + + JS::Rooted obj(cx, JS_NewUint8Array(cx, len)); + if (!obj) { + return nullptr; + } + + js::TypedArrayObject& ta = obj->as(); + if (ta.isSharedMemory()) { + // Must opt in to use shared memory. For now, don't. + // + // (It is incorrect to read into the buffer without + // synchronization since that can create a race. A + // lock here won't fix it - both sides must + // participate. So what one must do is to create a + // temporary buffer, read into that, and use a + // race-safe primitive to copy memory into the + // buffer.) + JS_ReportErrorUTF8(cx, "can't read %s: shared memory buffer", + pathname.get()); + return nullptr; + } + + char* buf = static_cast(ta.dataPointerUnshared()); + if (!ReadFile(cx, pathname.get(), file, buf, len)) { + return nullptr; + } + + return obj; +} + +/** + * Return the current working directory or |null| on failure. + */ +UniqueChars GetCWD(JSContext* cx) { +#ifdef XP_WIN + wchar_t buffer[PATH_MAX + 1]; + const wchar_t* cwd = _wgetcwd(buffer, PATH_MAX); + if (!cwd) { + return nullptr; + } + return JS::EncodeWideToUtf8(cx, buffer); +#else + char buffer[PATH_MAX + 1]; + const char* cwd = getcwd(buffer, PATH_MAX); + if (!cwd) { + return nullptr; + } + return JS::EncodeNarrowToUtf8(cx, buffer); +#endif +} + +static bool ReadFile(JSContext* cx, unsigned argc, Value* vp, + PathResolutionMode resolveMode) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1 || args.length() > 2) { + JS_ReportErrorNumberASCII( + cx, js::shell::my_GetErrorMessage, nullptr, + args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS, + "snarf"); + return false; + } + + if (!args[0].isString() || (args.length() == 2 && !args[1].isString())) { + JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "snarf"); + return false; + } + + JS::Rooted givenPath(cx, args[0].toString()); + JS::Rooted str(cx, + js::shell::ResolvePath(cx, givenPath, resolveMode)); + if (!str) { + return false; + } + + if (args.length() > 1) { + JSString* opt = JS::ToString(cx, args[1]); + if (!opt) { + return false; + } + bool match; + if (!JS_StringEqualsLiteral(cx, opt, "binary", &match)) { + return false; + } + if (match) { + JSObject* obj; + if (!(obj = FileAsTypedArray(cx, str))) { + return false; + } + args.rval().setObject(*obj); + return true; + } + } + + if (!(str = FileAsString(cx, str))) { + return false; + } + args.rval().setString(str); + return true; +} + +static bool osfile_readFile(JSContext* cx, unsigned argc, Value* vp) { + return ReadFile(cx, argc, vp, RootRelative); +} + +static bool osfile_readRelativeToScript(JSContext* cx, unsigned argc, + Value* vp) { + return ReadFile(cx, argc, vp, ScriptRelative); +} + +static bool ListDir(JSContext* cx, unsigned argc, Value* vp, + PathResolutionMode resolveMode) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "os.file.listDir requires 1 argument"); + return false; + } + + if (!args[0].isString()) { + JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "os.file.listDir"); + return false; + } + + RootedString givenPath(cx, args[0].toString()); + RootedString str(cx, ResolvePath(cx, givenPath, resolveMode)); + if (!str) { + return false; + } + + UniqueChars pathname = JS_EncodeStringToUTF8(cx, str); + if (!pathname) { + JS_ReportErrorASCII(cx, "os.file.listDir cannot convert path to Latin1"); + return false; + } + + RootedValueVector elems(cx); + auto append = [&](const char* name) -> bool { + if (!(str = JS_NewStringCopyZ(cx, name))) { + return false; + } + if (!elems.append(StringValue(str))) { + js::ReportOutOfMemory(cx); + return false; + } + return true; + }; + +#if defined(XP_UNIX) + { + DIR* dir = opendir(pathname.get()); + if (!dir) { + JS_ReportErrorASCII(cx, "os.file.listDir is unable to open: %s", + pathname.get()); + return false; + } + auto close = mozilla::MakeScopeExit([&] { + if (closedir(dir) != 0) { + MOZ_CRASH("Could not close dir"); + } + }); + + while (struct dirent* entry = readdir(dir)) { + if (!append(entry->d_name)) { + return false; + } + } + } +#elif defined(XP_WIN) + { + const size_t pathlen = strlen(pathname.get()); + Vector pattern(cx); + if (!pattern.append(pathname.get(), pathlen) || + !pattern.append(PathSeparator) || !pattern.append("*", 2)) { + js::ReportOutOfMemory(cx); + return false; + } + + WIN32_FIND_DATAA FindFileData; + HANDLE hFind = FindFirstFileA(pattern.begin(), &FindFileData); + auto close = mozilla::MakeScopeExit([&] { + if (!FindClose(hFind)) { + MOZ_CRASH("Could not close Find"); + } + }); + for (bool found = (hFind != INVALID_HANDLE_VALUE); found; + found = FindNextFileA(hFind, &FindFileData)) { + if (!append(FindFileData.cFileName)) { + return false; + } + } + } +#endif + + JSObject* array = JS::NewArrayObject(cx, elems); + if (!array) { + return false; + } + + args.rval().setObject(*array); + return true; +} + +static bool osfile_listDir(JSContext* cx, unsigned argc, Value* vp) { + return ListDir(cx, argc, vp, RootRelative); +} + +static bool osfile_listDirRelativeToScript(JSContext* cx, unsigned argc, + Value* vp) { + return ListDir(cx, argc, vp, ScriptRelative); +} + +static bool osfile_writeTypedArrayToFile(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 2 || !args[0].isString() || !args[1].isObject() || + !args[1].toObject().is()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "writeTypedArrayToFile"); + return false; + } + + RootedString givenPath(cx, args[0].toString()); + RootedString str(cx, ResolvePath(cx, givenPath, RootRelative)); + if (!str) { + return false; + } + + UniqueChars filename = JS_EncodeStringToUTF8(cx, str); + if (!filename) { + return false; + } + + FILE* file = OpenFile(cx, filename.get(), "wb"); + if (!file) { + return false; + } + AutoCloseFile autoClose(file); + + TypedArrayObject* obj = &args[1].toObject().as(); + + if (obj->isSharedMemory()) { + // Must opt in to use shared memory. For now, don't. + // + // See further comments in FileAsTypedArray, above. + JS_ReportErrorUTF8(cx, "can't write %s: shared memory buffer", + filename.get()); + return false; + } + void* buf = obj->dataPointerUnshared(); + size_t length = obj->length(); + if (fwrite(buf, obj->bytesPerElement(), length, file) != length || + !autoClose.release()) { + JS_ReportErrorUTF8(cx, "can't write %s", filename.get()); + return false; + } + + args.rval().setUndefined(); + return true; +} + +/* static */ +RCFile* RCFile::create(JSContext* cx, const char* filename, const char* mode) { + FILE* fp = OpenFile(cx, filename, mode); + if (!fp) { + return nullptr; + } + + RCFile* file = cx->new_(fp); + if (!file) { + fclose(fp); + return nullptr; + } + + return file; +} + +void RCFile::close() { + if (fp) { + fclose(fp); + } + fp = nullptr; +} + +bool RCFile::release() { + if (--numRefs) { + return false; + } + this->close(); + return true; +} + +class FileObject : public NativeObject { + enum : uint32_t { FILE_SLOT = 0, NUM_SLOTS }; + + public: + static const JSClass class_; + + static FileObject* create(JSContext* cx, RCFile* file) { + FileObject* obj = js::NewBuiltinClassInstance(cx); + if (!obj) { + return nullptr; + } + + InitReservedSlot(obj, FILE_SLOT, file, MemoryUse::FileObjectFile); + file->acquire(); + return obj; + } + + static void finalize(JS::GCContext* gcx, JSObject* obj) { + FileObject* fileObj = &obj->as(); + RCFile* file = fileObj->rcFile(); + gcx->removeCellMemory(obj, sizeof(*file), MemoryUse::FileObjectFile); + if (file->release()) { + gcx->deleteUntracked(file); + } + } + + bool isOpen() { + RCFile* file = rcFile(); + return file && file->isOpen(); + } + + void close() { + if (!isOpen()) { + return; + } + rcFile()->close(); + } + + RCFile* rcFile() { + return reinterpret_cast( + JS::GetReservedSlot(this, FILE_SLOT).toPrivate()); + } +}; + +static const JSClassOps FileObjectClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + FileObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass FileObject::class_ = { + "File", + JSCLASS_HAS_RESERVED_SLOTS(FileObject::NUM_SLOTS) | + JSCLASS_FOREGROUND_FINALIZE, + &FileObjectClassOps}; + +static FileObject* redirect(JSContext* cx, HandleString relFilename, + RCFile** globalFile) { + RootedString filename(cx, ResolvePath(cx, relFilename, RootRelative)); + if (!filename) { + return nullptr; + } + UniqueChars filenameABS = JS_EncodeStringToUTF8(cx, filename); + if (!filenameABS) { + return nullptr; + } + RCFile* file = RCFile::create(cx, filenameABS.get(), "wb"); + if (!file) { + return nullptr; + } + + // Grant the global gOutFile ownership of the new file, release ownership + // of its old file, and return a FileObject owning the old file. + file->acquire(); // Global owner of new file + + FileObject* fileObj = + FileObject::create(cx, *globalFile); // Newly created owner of old file + if (!fileObj) { + file->release(); + return nullptr; + } + + (*globalFile)->release(); // Release (global) ownership of old file. + *globalFile = file; + + return fileObj; +} + +static bool Redirect(JSContext* cx, const CallArgs& args, RCFile** outFile) { + if (args.length() > 1) { + JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "redirect"); + return false; + } + + RCFile* oldFile = *outFile; + RootedObject oldFileObj(cx, FileObject::create(cx, oldFile)); + if (!oldFileObj) { + return false; + } + + if (args.get(0).isUndefined()) { + args.rval().setObject(*oldFileObj); + return true; + } + + if (args[0].isObject()) { + Rooted fileObj(cx, + args[0].toObject().maybeUnwrapIf()); + if (!fileObj) { + JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "redirect"); + return false; + } + + // Passed in a FileObject. Create a FileObject for the previous + // global file, and set the global file to the passed-in one. + *outFile = fileObj->rcFile(); + (*outFile)->acquire(); + oldFile->release(); + + args.rval().setObject(*oldFileObj); + return true; + } + + RootedString filename(cx); + if (!args[0].isNull()) { + filename = JS::ToString(cx, args[0]); + if (!filename) { + return false; + } + } + + if (!redirect(cx, filename, outFile)) { + return false; + } + + args.rval().setObject(*oldFileObj); + return true; +} + +static bool osfile_redirectOutput(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + ShellContext* scx = GetShellContext(cx); + return Redirect(cx, args, scx->outFilePtr); +} + +static bool osfile_redirectError(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + ShellContext* scx = GetShellContext(cx); + return Redirect(cx, args, scx->errFilePtr); +} + +static bool osfile_close(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted fileObj(cx); + if (args.get(0).isObject()) { + fileObj = args[0].toObject().maybeUnwrapIf(); + } + + if (!fileObj) { + JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "close"); + return false; + } + + fileObj->close(); + + args.rval().setUndefined(); + return true; +} + +// clang-format off +static const JSFunctionSpecWithHelp osfile_functions[] = { + JS_FN_HELP("readFile", osfile_readFile, 1, 0, +"readFile(filename, [\"binary\"])", +" Read entire contents of filename. Returns a string, unless \"binary\" is passed\n" +" as the second argument, in which case it returns a Uint8Array. Filename is\n" +" relative to the current working directory."), + + JS_FN_HELP("readRelativeToScript", osfile_readRelativeToScript, 1, 0, +"readRelativeToScript(filename, [\"binary\"])", +" Read filename into returned string. Filename is relative to the directory\n" +" containing the current script."), + + JS_FN_HELP("listDir", osfile_listDir, 1, 0, +"listDir(dirname)", +" Read entire contents of a directory. The \"dirname\" parameter is relate to the\n" +" current working directory. Returns a list of filenames within the given directory.\n" +" Note that \".\" and \"..\" are also listed."), + + JS_FN_HELP("listDirRelativeToScript", osfile_listDirRelativeToScript, 1, 0, +"listDirRelativeToScript(dirname)", +" Same as \"listDir\" except that the \"dirname\" is relative to the directory\n" +" containing the current script."), + + JS_FS_HELP_END +}; +// clang-format on + +// clang-format off +static const JSFunctionSpecWithHelp osfile_unsafe_functions[] = { + JS_FN_HELP("writeTypedArrayToFile", osfile_writeTypedArrayToFile, 2, 0, +"writeTypedArrayToFile(filename, data)", +" Write the contents of a typed array to the named file."), + + JS_FN_HELP("redirect", osfile_redirectOutput, 1, 0, +"redirect([path-or-object])", +" Redirect print() output to the named file.\n" +" Return an opaque object representing the previous destination, which\n" +" may be passed into redirect() later to restore the output."), + + JS_FN_HELP("redirectErr", osfile_redirectError, 1, 0, +"redirectErr([path-or-object])", +" Same as redirect(), but for printErr"), + + JS_FN_HELP("close", osfile_close, 1, 0, +"close(object)", +" Close the file returned by an earlier redirect call."), + + JS_FS_HELP_END +}; +// clang-format on + +static bool ospath_isAbsolute(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isString()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "isAbsolute"); + return false; + } + + Rooted str(cx, + JS_EnsureLinearString(cx, args[0].toString())); + if (!str) { + return false; + } + + args.rval().setBoolean(IsAbsolutePath(str)); + return true; +} + +static bool ospath_join(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "join"); + return false; + } + + // This function doesn't take into account some aspects of Windows paths, + // e.g. the drive letter is always reset when an absolute path is appended. + + JSStringBuilder buffer(cx); + Rooted str(cx); + + for (unsigned i = 0; i < args.length(); i++) { + if (!args[i].isString()) { + JS_ReportErrorASCII(cx, "join expects string arguments only"); + return false; + } + + str = JS_EnsureLinearString(cx, args[i].toString()); + if (!str) { + return false; + } + + if (IsAbsolutePath(str)) { + MOZ_ALWAYS_TRUE(buffer.resize(0)); + } else if (i != 0) { + UniqueChars path = JS_EncodeStringToUTF8(cx, str); + if (!path) { + return false; + } + + if (!buffer.append(PathSeparator)) { + return false; + } + } + + if (!buffer.append(args[i].toString())) { + return false; + } + } + + JSString* result = buffer.finishString(); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +// clang-format off +static const JSFunctionSpecWithHelp ospath_functions[] = { + JS_FN_HELP("isAbsolute", ospath_isAbsolute, 1, 0, +"isAbsolute(path)", +" Return whether the given path is absolute."), + + JS_FN_HELP("join", ospath_join, 1, 0, +"join(paths...)", +" Join one or more path components in a platform independent way."), + + JS_FS_HELP_END +}; +// clang-format on + +static bool os_getenv(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() < 1) { + JS_ReportErrorASCII(cx, "os.getenv requires 1 argument"); + return false; + } + RootedString key(cx, ToString(cx, args[0])); + if (!key) { + return false; + } + UniqueChars keyBytes = JS_EncodeStringToUTF8(cx, key); + if (!keyBytes) { + return false; + } + + if (const char* valueBytes = getenv(keyBytes.get())) { + RootedString value(cx, JS_NewStringCopyZ(cx, valueBytes)); + if (!value) { + return false; + } + args.rval().setString(value); + } else { + args.rval().setUndefined(); + } + return true; +} + +static bool os_getpid(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 0) { + JS_ReportErrorASCII(cx, "os.getpid takes no arguments"); + return false; + } + args.rval().setInt32(getpid()); + return true; +} + +#if !defined(XP_WIN) + +// There are two possible definitions of strerror_r floating around. The GNU +// one returns a char* which may or may not be the buffer you passed in. The +// other one returns an integer status code, and always writes the result into +// the provided buffer. + +inline char* strerror_message(int result, char* buffer) { + return result == 0 ? buffer : nullptr; +} + +inline char* strerror_message(char* result, char* buffer) { return result; } + +#endif + +UniqueChars SystemErrorMessage(JSContext* cx, int errnum) { +#if defined(XP_WIN) + wchar_t buffer[200]; + const wchar_t* errstr = buffer; + if (_wcserror_s(buffer, mozilla::ArrayLength(buffer), errnum) != 0) { + errstr = L"unknown error"; + } + return JS::EncodeWideToUtf8(cx, errstr); +#else + char buffer[200]; + const char* errstr = strerror_message( + strerror_r(errno, buffer, mozilla::ArrayLength(buffer)), buffer); + if (!errstr) { + errstr = "unknown error"; + } + return JS::EncodeNarrowToUtf8(cx, errstr); +#endif +} + +#ifndef __wasi__ +static void ReportSysError(JSContext* cx, const char* prefix) { + MOZ_ASSERT(JS::StringIsASCII(prefix)); + + if (UniqueChars error = SystemErrorMessage(cx, errno)) { + JS_ReportErrorUTF8(cx, "%s: %s", prefix, error.get()); + } +} + +static bool os_system(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + JS_ReportErrorASCII(cx, "os.system requires 1 argument"); + return false; + } + + Rooted str(cx, JS::ToString(cx, args[0])); + if (!str) { + return false; + } + + UniqueChars command = JS_EncodeStringToUTF8(cx, str); + if (!command) { + return false; + } + +# ifdef XP_WIN + UniqueWideChars wideCommand = JS::EncodeUtf8ToWide(cx, command.get()); + if (!wideCommand) { + return false; + } + + // Existing streams must be explicitly flushed or closed before calling + // the system() function on Windows. + _flushall(); + + int result = _wsystem(wideCommand.get()); +# else + UniqueChars narrowCommand = JS::EncodeUtf8ToNarrow(cx, command.get()); + if (!narrowCommand) { + return false; + } + + int result = system(narrowCommand.get()); +# endif + if (result == -1) { + ReportSysError(cx, "system call failed"); + return false; + } + + args.rval().setInt32(result); + return true; +} + +# ifndef XP_WIN +static bool os_spawn(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + JS_ReportErrorASCII(cx, "os.spawn requires 1 argument"); + return false; + } + + Rooted str(cx, JS::ToString(cx, args[0])); + if (!str) { + return false; + } + + UniqueChars command = JS_EncodeStringToUTF8(cx, str); + if (!command) { + return false; + } + UniqueChars narrowCommand = JS::EncodeUtf8ToNarrow(cx, command.get()); + if (!narrowCommand) { + return false; + } + + int32_t childPid = fork(); + if (childPid == -1) { + ReportSysError(cx, "fork failed"); + return false; + } + + if (childPid) { + args.rval().setInt32(childPid); + return true; + } + + // We are in the child + + const char* cmd[] = {"sh", "-c", nullptr, nullptr}; + cmd[2] = narrowCommand.get(); + + execvp("sh", (char* const*)cmd); + exit(1); +} + +static bool os_kill(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + int32_t pid; + if (args.length() < 1) { + JS_ReportErrorASCII(cx, "os.kill requires 1 argument"); + return false; + } + if (!JS::ToInt32(cx, args[0], &pid)) { + return false; + } + + // It is too easy to kill yourself accidentally with os.kill("goose"). + if (pid == 0 && !args[0].isInt32()) { + JS_ReportErrorASCII(cx, "os.kill requires numeric pid"); + return false; + } + + int signal = SIGINT; + if (args.length() > 1) { + if (!JS::ToInt32(cx, args[1], &signal)) { + return false; + } + } + + int status = kill(pid, signal); + if (status == -1) { + ReportSysError(cx, "kill failed"); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool os_waitpid(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + int32_t pid; + if (args.length() == 0) { + pid = -1; + } else { + if (!JS::ToInt32(cx, args[0], &pid)) { + return false; + } + } + + bool nohang = false; + if (args.length() >= 2) { + nohang = JS::ToBoolean(args[1]); + } + + int status = 0; + pid_t result = waitpid(pid, &status, nohang ? WNOHANG : 0); + if (result == -1) { + ReportSysError(cx, "os.waitpid failed"); + return false; + } + + RootedObject info(cx, JS_NewPlainObject(cx)); + if (!info) { + return false; + } + + RootedValue v(cx); + if (result != 0) { + v.setInt32(result); + if (!JS_DefineProperty(cx, info, "pid", v, JSPROP_ENUMERATE)) { + return false; + } + if (WIFEXITED(status)) { + v.setInt32(WEXITSTATUS(status)); + if (!JS_DefineProperty(cx, info, "exitStatus", v, JSPROP_ENUMERATE)) { + return false; + } + } + } + + args.rval().setObject(*info); + return true; +} +# endif +#endif // !__wasi__ + +// clang-format off +static const JSFunctionSpecWithHelp os_functions[] = { + JS_FN_HELP("getenv", os_getenv, 1, 0, +"getenv(variable)", +" Get the value of an environment variable."), + + JS_FN_HELP("getpid", os_getpid, 0, 0, +"getpid()", +" Return the current process id."), + +#ifndef __wasi__ + JS_FN_HELP("system", os_system, 1, 0, +"system(command)", +" Execute command on the current host, returning result code or throwing an\n" +" exception on failure."), + +# ifndef XP_WIN + JS_FN_HELP("spawn", os_spawn, 1, 0, +"spawn(command)", +" Start up a separate process running the given command. Returns the pid."), + + JS_FN_HELP("kill", os_kill, 1, 0, +"kill(pid[, signal])", +" Send a signal to the given pid. The default signal is SIGINT. The signal\n" +" passed in must be numeric, if given."), + + JS_FN_HELP("waitpid", os_waitpid, 1, 0, +"waitpid(pid[, nohang])", +" Calls waitpid(). 'nohang' is a boolean indicating whether to pass WNOHANG.\n" +" The return value is an object containing a 'pid' field, if a process was waitable\n" +" and an 'exitStatus' field if a pid exited."), +# endif +#endif // !__wasi__ + + JS_FS_HELP_END +}; +// clang-format on + +bool DefineOS(JSContext* cx, HandleObject global, bool fuzzingSafe, + RCFile** shellOut, RCFile** shellErr) { + RootedObject obj(cx, JS_NewPlainObject(cx)); + if (!obj || !JS_DefineProperty(cx, global, "os", obj, 0)) { + return false; + } + + if (!fuzzingSafe) { + if (!JS_DefineFunctionsWithHelp(cx, obj, os_functions)) { + return false; + } + } + + RootedObject osfile(cx, JS_NewPlainObject(cx)); + if (!osfile || !JS_DefineFunctionsWithHelp(cx, osfile, osfile_functions) || + !JS_DefineProperty(cx, obj, "file", osfile, 0)) { + return false; + } + + if (!fuzzingSafe) { + if (!JS_DefineFunctionsWithHelp(cx, osfile, osfile_unsafe_functions)) { + return false; + } + } + + if (!GenerateInterfaceHelp(cx, osfile, "os.file")) { + return false; + } + + RootedObject ospath(cx, JS_NewPlainObject(cx)); + if (!ospath || !JS_DefineFunctionsWithHelp(cx, ospath, ospath_functions) || + !JS_DefineProperty(cx, obj, "path", ospath, 0) || + !GenerateInterfaceHelp(cx, ospath, "os.path")) { + return false; + } + + if (!GenerateInterfaceHelp(cx, obj, "os")) { + return false; + } + + ShellContext* scx = GetShellContext(cx); + scx->outFilePtr = shellOut; + scx->errFilePtr = shellErr; + + // For backwards compatibility, expose various os.file.* functions as + // direct methods on the global. + struct Export { + const char* src; + const char* dst; + }; + + const Export osfile_exports[] = { + {"readFile", "read"}, + {"readFile", "snarf"}, + {"readRelativeToScript", "readRelativeToScript"}, + }; + + for (auto pair : osfile_exports) { + if (!CreateAlias(cx, pair.dst, osfile, pair.src)) { + return false; + } + } + + if (!fuzzingSafe) { + const Export unsafe_osfile_exports[] = {{"redirect", "redirect"}, + {"redirectErr", "redirectErr"}}; + + for (auto pair : unsafe_osfile_exports) { + if (!CreateAlias(cx, pair.dst, osfile, pair.src)) { + return false; + } + } + } + + return true; +} + +} // namespace shell +} // namespace js diff --git a/js/src/shell/OSObject.h b/js/src/shell/OSObject.h new file mode 100644 index 0000000000..df50d0a37d --- /dev/null +++ b/js/src/shell/OSObject.h @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// OSObject.h - os object for exposing posix system calls in the JS shell + +#ifndef shell_OSObject_h +#define shell_OSObject_h + +#include + +#include "js/TypeDecls.h" +#include "js/Utility.h" + +class JSLinearString; + +namespace js { +namespace shell { + +#ifdef XP_WIN +constexpr char PathSeparator = '\\'; +#else +constexpr char PathSeparator = '/'; +#endif + +struct RCFile; + +/* Define an os object on the given global object. */ +bool DefineOS(JSContext* cx, JS::HandleObject global, bool fuzzingSafe, + RCFile** shellOut, RCFile** shellErr); + +enum PathResolutionMode { RootRelative, ScriptRelative }; + +bool IsAbsolutePath(JSLinearString* filename); + +JSString* ResolvePath(JSContext* cx, JS::HandleString filenameStr, + PathResolutionMode resolveMode); + +JSObject* FileAsTypedArray(JSContext* cx, JS::HandleString pathnameStr); + +/** + * Return the current working directory as a UTF-8 encoded string. + * + * @param cx current js-context + * @return the working directory name or {@code nullptr} on error + */ +JS::UniqueChars GetCWD(JSContext* cx); + +/** + * Open the requested file. + * + * @param cx current js-context + * @param filename file name encoded in UTF-8 + * @param mode file mode specifier, see {@code fopen} for valid values + * @return a FILE pointer or {@code nullptr} on failure + */ +FILE* OpenFile(JSContext* cx, const char* filename, const char* mode); + +/** + * Read {@code length} bytes in the given buffer. + * + * @param cx current js-context + * @param filename file name encoded in UTF-8, only used for error reporting + * @param file file pointer to read from + * @param buffer destination buffer to copy read bytes into + * @param length number of bytes to read + * @return returns false and reports an error if not exactly {@code length} + * bytes could be read from the input file + */ +bool ReadFile(JSContext* cx, const char* filename, FILE* file, char* buffer, + size_t length); + +/** + * Compute the file size in bytes. + * + * @param cx current js-context + * @param filename file name encoded in UTF-8, only used for error reporting + * @param file file object to inspect + * @param size output parameter to store the file size into + * @return returns false and reports an error if an I/O error occurred + */ +bool FileSize(JSContext* cx, const char* filename, FILE* file, size_t* size); + +/** + * Return the system error message for the given error number. The error + * message is UTF-8 encoded. + * + * @param cx current js-context + * @param errnum error number + * @return error message or {@code nullptr} on error + */ +JS::UniqueChars SystemErrorMessage(JSContext* cx, int errnum); + +} // namespace shell +} // namespace js + +#endif /* shell_OSObject_h */ diff --git a/js/src/shell/ShellModuleObjectWrapper.cpp b/js/src/shell/ShellModuleObjectWrapper.cpp new file mode 100644 index 0000000000..4bc10d846d --- /dev/null +++ b/js/src/shell/ShellModuleObjectWrapper.cpp @@ -0,0 +1,469 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 + * -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "shell/ShellModuleObjectWrapper.h" + +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" + +#include "jsapi.h" // JS_GetProperty, JS::Call, JS_NewPlainObject, JS_DefineProperty + +#include "builtin/ModuleObject.h" // js::ModuleObject +#include "js/CallAndConstruct.h" // JS::Call +#include "js/CallArgs.h" // JS::CallArgs +#include "js/CallNonGenericMethod.h" // CallNonGenericMethod +#include "js/Class.h" // JSClass, JSCLASS_* +#include "js/ErrorReport.h" // JS_ReportErrorASCII +#include "js/PropertyAndElement.h" // JS_GetProperty +#include "js/PropertySpec.h" // JSPropertySpec, JS_PSG, JS_PS_END, JSFunctionSpec, JS_FN, JS_FN_END +#include "js/RootingAPI.h" // JS::Rooted, JS::Handle, JS::MutableHandle +#include "js/Value.h" // JS::Value +#include "vm/ArrayObject.h" // ArrayObject, NewDenseFullyAllocatedArray +#include "vm/GlobalObject.h" // DefinePropertiesAndFunctions +#include "vm/JSFunction.h" // JSFunction +#include "vm/JSObject.h" // JSObject +#include "vm/List.h" // ListObject +#include "vm/NativeObject.h" // NativeObject +#include "vm/Stack.h" // FixedInvokeArgs + +#include "vm/NativeObject-inl.h" // NativeObject::ensureDenseInitializedLength + +using namespace js; +using namespace js::shell; + +using mozilla::Span; + +#define DEFINE_CLASS_IMPL(CLASS) \ + CLASS* Shell##CLASS##Wrapper::get() { \ + return &getReservedSlot(TargetSlot).toObject().as(); \ + } \ + /* static */ const JSClass Shell##CLASS##Wrapper::class_ = { \ + "Shell" #CLASS "Wrapper", \ + JSCLASS_HAS_RESERVED_SLOTS(Shell##CLASS##Wrapper::SlotCount)}; \ + MOZ_ALWAYS_INLINE bool IsShell##CLASS##Wrapper(JS::Handle v) { \ + return v.isObject() && v.toObject().is(); \ + } + +#define DEFINE_CLASS(CLASS) \ + class Shell##CLASS##Wrapper : public js::NativeObject { \ + public: \ + using Target = CLASS; \ + enum ModuleSlot { TargetSlot = 0, SlotCount }; \ + static const JSClass class_; \ + static Shell##CLASS##Wrapper* create(JSContext* cx, \ + JS::Handle obj); \ + CLASS* get(); \ + }; \ + DEFINE_CLASS_IMPL(CLASS) + +#define DEFINE_NATIVE_CLASS_IMPL(CLASS) \ + CLASS* Shell##CLASS##Wrapper::get() { \ + return static_cast(getReservedSlot(TargetSlot).toPrivate()); \ + } \ + /* static */ const JSClass Shell##CLASS##Wrapper::class_ = { \ + "Shell" #CLASS "Wrapper", \ + JSCLASS_HAS_RESERVED_SLOTS(Shell##CLASS##Wrapper::SlotCount)}; \ + MOZ_ALWAYS_INLINE bool IsShell##CLASS##Wrapper(JS::Handle v) { \ + return v.isObject() && v.toObject().is(); \ + } + +#define DEFINE_NATIVE_CLASS(CLASS) \ + class Shell##CLASS##Wrapper : public js::NativeObject { \ + public: \ + using Target = CLASS; \ + enum ModuleSlot { OwnerSlot = 0, TargetSlot, SlotCount }; \ + static const JSClass class_; \ + static Shell##CLASS##Wrapper* create(JSContext* cx, \ + JS::Handle owner, \ + CLASS* obj); \ + CLASS* get(); \ + }; \ + DEFINE_NATIVE_CLASS_IMPL(CLASS) + +DEFINE_CLASS(ModuleRequestObject) +DEFINE_NATIVE_CLASS(ImportEntry) +DEFINE_NATIVE_CLASS(ExportEntry) +DEFINE_NATIVE_CLASS(RequestedModule) +// NOTE: We don't need wrapper for IndirectBindingMap and ModuleNamespaceObject +DEFINE_CLASS_IMPL(ModuleObject) + +#undef DEFINE_CLASS +#undef DEFINE_CLASS_IMPL +#undef DEFINE_NATIVE_CLASS +#undef DEFINE_NATIVE_CLASS_IMPL + +bool IdentFilter(JSContext* cx, JS::Handle from, + JS::MutableHandle to) { + to.set(from); + return true; +} + +template +bool SingleFilter(JSContext* cx, JS::Handle from, + JS::MutableHandle to) { + using TargetT = typename T::Target; + + if (!from.isObject() || !from.toObject().is()) { + to.set(from); + return true; + } + + JS::Rooted obj(cx, &from.toObject().as()); + JS::Rooted filtered(cx, T::create(cx, obj)); + if (!filtered) { + return false; + } + to.setObject(*filtered); + return true; +} + +template +bool ArrayFilter(JSContext* cx, JS::Handle from, + JS::MutableHandle to) { + using TargetT = typename T::Target; + + if (!from.isObject() || !from.toObject().is()) { + to.set(from); + return true; + } + + JS::Rooted fromArray(cx, &from.toObject().as()); + uint32_t length = fromArray->length(); + JS::Rooted toArray(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!toArray) { + return false; + } + + toArray->ensureDenseInitializedLength(0, length); + + for (uint32_t i = 0; i < length; i++) { + JS::Rooted item(cx, fromArray->getDenseElement(i)); + JS::Rooted req(cx, &item.toObject().as()); + JS::Rooted filtered(cx, T::create(cx, req)); + if (!filtered) { + return false; + } + toArray->initDenseElement(i, ObjectValue(*filtered)); + } + to.setObject(*toArray); + return true; +} + +template +bool ListToArrayFilter(JSContext* cx, JS::Handle from, + JS::MutableHandle to) { + using TargetT = typename T::Target; + + if (!from.isObject() || !from.toObject().is()) { + to.set(from); + return true; + } + + JS::Rooted fromList(cx, &from.toObject().as()); + uint32_t length = fromList->length(); + JS::Rooted toArray(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!toArray) { + return false; + } + + toArray->ensureDenseInitializedLength(0, length); + + for (uint32_t i = 0; i < length; i++) { + JS::Rooted item(cx, fromList->get(i)); + JS::Rooted req(cx, &item.toObject().as()); + JS::Rooted filtered(cx, T::create(cx, req)); + if (!filtered) { + return false; + } + toArray->initDenseElement(i, ObjectValue(*filtered)); + } + to.setObject(*toArray); + return true; +} + +static Value StringOrNullValue(JSString* maybeString) { + if (!maybeString) { + return NullValue(); + } + + return StringValue(maybeString); +} + +static Value Uint32Value(uint32_t x) { + MOZ_ASSERT(x <= INT32_MAX); + return Int32Value(x); +} + +static Value Uint32OrUndefinedValue(mozilla::Maybe x) { + if (x.isNothing()) { + return UndefinedValue(); + } + + return Uint32Value(x.value()); +} + +static Value StatusValue(ModuleStatus status) { + return Int32Value(int32_t(status)); +} + +static Value ObjectOrUndefinedValue(JSObject* object) { + if (!object) { + return UndefinedValue(); + } + + return ObjectValue(*object); +} + +template +bool ShellModuleWrapperGetter(JSContext* cx, const JS::CallArgs& args, + RawGetterT rawGetter, FilterT filter) { + JS::Rooted wrapper(cx, &args.thisv().toObject().as()); + JS::Rooted raw(cx, rawGetter(wrapper->get())); + + JS::Rooted filtered(cx); + if (!filter(cx, raw, &filtered)) { + return false; + } + + args.rval().set(filtered); + return true; +} + +#define DEFINE_GETTER_FUNCTIONS(CLASS, PROP, TO_VALUE, FILTER) \ + static Value Shell##CLASS##Wrapper_##PROP##Getter_raw(CLASS* obj) { \ + return TO_VALUE(obj->PROP()); \ + } \ + static bool Shell##CLASS##Wrapper_##PROP##Getter_impl( \ + JSContext* cx, const JS::CallArgs& args) { \ + return ShellModuleWrapperGetter( \ + cx, args, Shell##CLASS##Wrapper_##PROP##Getter_raw, FILTER); \ + } \ + static bool Shell##CLASS##Wrapper_##PROP##Getter(JSContext* cx, \ + unsigned argc, Value* vp) { \ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \ + return CallNonGenericMethod( \ + cx, args); \ + } + +template +bool SpanToArrayFilter(JSContext* cx, JS::Handle owner, + Span from, + JS::MutableHandle to) { + size_t length = from.Length(); + JS::Rooted toArray(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!toArray) { + return false; + } + + toArray->ensureDenseInitializedLength(0, length); + + for (uint32_t i = 0; i < length; i++) { + auto* element = const_cast(&from[i]); + JS::Rooted filtered(cx, T::create(cx, owner, element)); + if (!filtered) { + return false; + } + toArray->initDenseElement(i, ObjectValue(*filtered)); + } + + to.setObject(*toArray); + return true; +} + +template +bool ShellModuleNativeWrapperGetter(JSContext* cx, const JS::CallArgs& args, + RawGetterT rawGetter, FilterT filter) { + JS::Rooted wrapper(cx, &args.thisv().toObject().as()); + JS::Rooted owner(cx, wrapper->get()); + + JS::Rooted filtered(cx); + if (!filter(cx, owner, rawGetter(owner), &filtered)) { + return false; + } + + args.rval().set(filtered); + return true; +} + +#define DEFINE_NATIVE_GETTER_FUNCTIONS(CLASS, PROP, FILTER) \ + static auto Shell##CLASS##Wrapper_##PROP##Getter_raw(CLASS* obj) { \ + return obj->PROP(); \ + } \ + static bool Shell##CLASS##Wrapper_##PROP##Getter_impl( \ + JSContext* cx, const JS::CallArgs& args) { \ + return ShellModuleNativeWrapperGetter( \ + cx, args, Shell##CLASS##Wrapper_##PROP##Getter_raw, FILTER); \ + } \ + static bool Shell##CLASS##Wrapper_##PROP##Getter(JSContext* cx, \ + unsigned argc, Value* vp) { \ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \ + return CallNonGenericMethod( \ + cx, args); \ + } + +DEFINE_GETTER_FUNCTIONS(ModuleRequestObject, specifier, StringOrNullValue, + IdentFilter) +DEFINE_GETTER_FUNCTIONS(ModuleRequestObject, assertions, ObjectOrNullValue, + IdentFilter) + +static const JSPropertySpec ShellModuleRequestObjectWrapper_accessors[] = { + JS_PSG("specifier", ShellModuleRequestObjectWrapper_specifierGetter, 0), + JS_PSG("assertions", ShellModuleRequestObjectWrapper_assertionsGetter, 0), + JS_PS_END}; + +DEFINE_GETTER_FUNCTIONS(ImportEntry, moduleRequest, ObjectOrNullValue, + SingleFilter) +DEFINE_GETTER_FUNCTIONS(ImportEntry, importName, StringOrNullValue, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ImportEntry, localName, StringValue, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ImportEntry, lineNumber, Uint32Value, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ImportEntry, columnNumber, Uint32Value, IdentFilter) + +static const JSPropertySpec ShellImportEntryWrapper_accessors[] = { + JS_PSG("moduleRequest", ShellImportEntryWrapper_moduleRequestGetter, 0), + JS_PSG("importName", ShellImportEntryWrapper_importNameGetter, 0), + JS_PSG("localName", ShellImportEntryWrapper_localNameGetter, 0), + JS_PSG("lineNumber", ShellImportEntryWrapper_lineNumberGetter, 0), + JS_PSG("columnNumber", ShellImportEntryWrapper_columnNumberGetter, 0), + JS_PS_END}; + +DEFINE_GETTER_FUNCTIONS(ExportEntry, exportName, StringOrNullValue, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ExportEntry, moduleRequest, ObjectOrNullValue, + SingleFilter) +DEFINE_GETTER_FUNCTIONS(ExportEntry, importName, StringOrNullValue, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ExportEntry, localName, StringOrNullValue, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ExportEntry, lineNumber, Uint32Value, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ExportEntry, columnNumber, Uint32Value, IdentFilter) + +static const JSPropertySpec ShellExportEntryWrapper_accessors[] = { + JS_PSG("exportName", ShellExportEntryWrapper_exportNameGetter, 0), + JS_PSG("moduleRequest", ShellExportEntryWrapper_moduleRequestGetter, 0), + JS_PSG("importName", ShellExportEntryWrapper_importNameGetter, 0), + JS_PSG("localName", ShellExportEntryWrapper_localNameGetter, 0), + JS_PSG("lineNumber", ShellExportEntryWrapper_lineNumberGetter, 0), + JS_PSG("columnNumber", ShellExportEntryWrapper_columnNumberGetter, 0), + JS_PS_END}; + +DEFINE_GETTER_FUNCTIONS(RequestedModule, moduleRequest, ObjectOrNullValue, + SingleFilter) +DEFINE_GETTER_FUNCTIONS(RequestedModule, lineNumber, Uint32Value, IdentFilter) +DEFINE_GETTER_FUNCTIONS(RequestedModule, columnNumber, Uint32Value, IdentFilter) + +static const JSPropertySpec ShellRequestedModuleWrapper_accessors[] = { + JS_PSG("moduleRequest", ShellRequestedModuleWrapper_moduleRequestGetter, 0), + JS_PSG("lineNumber", ShellRequestedModuleWrapper_lineNumberGetter, 0), + JS_PSG("columnNumber", ShellRequestedModuleWrapper_columnNumberGetter, 0), + JS_PS_END}; + +DEFINE_GETTER_FUNCTIONS(ModuleObject, namespace_, ObjectOrNullValue, + IdentFilter) +DEFINE_GETTER_FUNCTIONS(ModuleObject, status, StatusValue, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ModuleObject, maybeEvaluationError, Value, IdentFilter) +DEFINE_NATIVE_GETTER_FUNCTIONS(ModuleObject, requestedModules, + SpanToArrayFilter) +DEFINE_NATIVE_GETTER_FUNCTIONS(ModuleObject, importEntries, + SpanToArrayFilter) +DEFINE_NATIVE_GETTER_FUNCTIONS(ModuleObject, localExportEntries, + SpanToArrayFilter) +DEFINE_NATIVE_GETTER_FUNCTIONS(ModuleObject, indirectExportEntries, + SpanToArrayFilter) +DEFINE_NATIVE_GETTER_FUNCTIONS(ModuleObject, starExportEntries, + SpanToArrayFilter) +DEFINE_GETTER_FUNCTIONS(ModuleObject, maybeDfsIndex, Uint32OrUndefinedValue, + IdentFilter) +DEFINE_GETTER_FUNCTIONS(ModuleObject, maybeDfsAncestorIndex, + Uint32OrUndefinedValue, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ModuleObject, hasTopLevelAwait, BooleanValue, + IdentFilter) +DEFINE_GETTER_FUNCTIONS(ModuleObject, maybeTopLevelCapability, + ObjectOrUndefinedValue, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ModuleObject, isAsyncEvaluating, BooleanValue, + IdentFilter) +DEFINE_GETTER_FUNCTIONS(ModuleObject, maybeAsyncEvaluatingPostOrder, + Uint32OrUndefinedValue, IdentFilter) +DEFINE_GETTER_FUNCTIONS(ModuleObject, asyncParentModules, ObjectOrNullValue, + ListToArrayFilter) +DEFINE_GETTER_FUNCTIONS(ModuleObject, maybePendingAsyncDependencies, + Uint32OrUndefinedValue, IdentFilter) + +static const JSPropertySpec ShellModuleObjectWrapper_accessors[] = { + JS_PSG("namespace", ShellModuleObjectWrapper_namespace_Getter, 0), + JS_PSG("status", ShellModuleObjectWrapper_statusGetter, 0), + JS_PSG("evaluationError", + ShellModuleObjectWrapper_maybeEvaluationErrorGetter, 0), + JS_PSG("requestedModules", ShellModuleObjectWrapper_requestedModulesGetter, + 0), + JS_PSG("importEntries", ShellModuleObjectWrapper_importEntriesGetter, 0), + JS_PSG("localExportEntries", + ShellModuleObjectWrapper_localExportEntriesGetter, 0), + JS_PSG("indirectExportEntries", + ShellModuleObjectWrapper_indirectExportEntriesGetter, 0), + JS_PSG("starExportEntries", + ShellModuleObjectWrapper_starExportEntriesGetter, 0), + JS_PSG("dfsIndex", ShellModuleObjectWrapper_maybeDfsIndexGetter, 0), + JS_PSG("dfsAncestorIndex", + ShellModuleObjectWrapper_maybeDfsAncestorIndexGetter, 0), + JS_PSG("hasTopLevelAwait", ShellModuleObjectWrapper_hasTopLevelAwaitGetter, + 0), + JS_PSG("topLevelCapability", + ShellModuleObjectWrapper_maybeTopLevelCapabilityGetter, 0), + JS_PSG("isAsyncEvaluating", + ShellModuleObjectWrapper_isAsyncEvaluatingGetter, 0), + JS_PSG("asyncEvaluatingPostOrder", + ShellModuleObjectWrapper_maybeAsyncEvaluatingPostOrderGetter, 0), + JS_PSG("asyncParentModules", + ShellModuleObjectWrapper_asyncParentModulesGetter, 0), + JS_PSG("pendingAsyncDependencies", + ShellModuleObjectWrapper_maybePendingAsyncDependenciesGetter, 0), + JS_PS_END}; + +#undef DEFINE_GETTER_FUNCTIONS +#undef DEFINE_NATIVE_GETTER_FUNCTIONS + +#define DEFINE_CREATE(CLASS, ACCESSORS, FUNCTIONS) \ + /* static */ \ + Shell##CLASS##Wrapper* Shell##CLASS##Wrapper::create( \ + JSContext* cx, JS::Handle target) { \ + JS::Rooted obj(cx, JS_NewObject(cx, &class_)); \ + if (!obj) { \ + return nullptr; \ + } \ + if (!DefinePropertiesAndFunctions(cx, obj, ACCESSORS, FUNCTIONS)) { \ + return nullptr; \ + } \ + auto* wrapper = &obj->as(); \ + wrapper->initReservedSlot(TargetSlot, ObjectValue(*target)); \ + return wrapper; \ + } + +#define DEFINE_NATIVE_CREATE(CLASS, ACCESSORS, FUNCTIONS) \ + /* static */ \ + Shell##CLASS##Wrapper* Shell##CLASS##Wrapper::create( \ + JSContext* cx, JS::Handle owner, CLASS* target) { \ + JS::Rooted obj(cx, JS_NewObject(cx, &class_)); \ + if (!obj) { \ + return nullptr; \ + } \ + if (!DefinePropertiesAndFunctions(cx, obj, ACCESSORS, FUNCTIONS)) { \ + return nullptr; \ + } \ + auto* wrapper = &obj->as(); \ + wrapper->initReservedSlot(OwnerSlot, ObjectValue(*owner)); \ + wrapper->initReservedSlot(TargetSlot, PrivateValue(target)); \ + return wrapper; \ + } + +DEFINE_CREATE(ModuleRequestObject, ShellModuleRequestObjectWrapper_accessors, + nullptr) +DEFINE_NATIVE_CREATE(ImportEntry, ShellImportEntryWrapper_accessors, nullptr) +DEFINE_NATIVE_CREATE(ExportEntry, ShellExportEntryWrapper_accessors, nullptr) +DEFINE_NATIVE_CREATE(RequestedModule, ShellRequestedModuleWrapper_accessors, + nullptr) +DEFINE_CREATE(ModuleObject, ShellModuleObjectWrapper_accessors, nullptr) + +#undef DEFINE_CREATE +#undef DEFINE_NATIVE_CREATE diff --git a/js/src/shell/ShellModuleObjectWrapper.h b/js/src/shell/ShellModuleObjectWrapper.h new file mode 100644 index 0000000000..8e0fd20987 --- /dev/null +++ b/js/src/shell/ShellModuleObjectWrapper.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef shell_ShellModuleObjectWrapper_h +#define shell_ShellModuleObjectWrapper_h + +#include "builtin/ModuleObject.h" // js::ModuleObject +#include "js/Class.h" // JSClass +#include "js/RootingAPI.h" // JS::Handle +#include "vm/NativeObject.h" // js::NativeObject + +namespace js { + +namespace shell { + +// ModuleObject's accessors and methods are only for internal usage in +// js/src/builtin/Module.js, and they don't check arguments types. +// +// To use ModuleObject in tests, add a wrapper that checks arguments types. +class ShellModuleObjectWrapper : public js::NativeObject { + public: + using Target = ModuleObject; + enum ModuleSlot { TargetSlot = 0, SlotCount }; + static const JSClass class_; + static ShellModuleObjectWrapper* create(JSContext* cx, + JS::Handle moduleObj); + ModuleObject* get(); +}; + +} // namespace shell +} // namespace js + +#endif /* shell_ShellModuleObjectWrapper_h */ diff --git a/js/src/shell/StringUtils.h b/js/src/shell/StringUtils.h new file mode 100644 index 0000000000..1267c81eee --- /dev/null +++ b/js/src/shell/StringUtils.h @@ -0,0 +1,146 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* String utility functions used by the module loader. */ + +#ifndef shell_StringUtils_h +#define shell_StringUtils_h + +#include "js/StableStringChars.h" +#include "js/String.h" + +namespace js { +namespace shell { + +inline char16_t CharAt(JSLinearString* str, size_t index) { + return str->latin1OrTwoByteChar(index); +} + +inline JSLinearString* SubString(JSContext* cx, JSLinearString* str, + size_t start, size_t end) { + MOZ_ASSERT(start <= str->length()); + MOZ_ASSERT(end >= start && end <= str->length()); + return NewDependentString(cx, str, start, end - start); +} + +inline JSLinearString* SubString(JSContext* cx, JSLinearString* str, + size_t start) { + return SubString(cx, str, start, str->length()); +} + +template +bool StringStartsWith(JSLinearString* str, + const char16_t (&chars)[NullTerminatedLength]) { + MOZ_ASSERT(NullTerminatedLength > 0); + const size_t length = NullTerminatedLength - 1; + MOZ_ASSERT(chars[length] == '\0'); + + if (str->length() < length) { + return false; + } + + for (size_t i = 0; i < length; i++) { + if (CharAt(str, i) != chars[i]) { + return false; + } + } + + return true; +} + +template +bool StringEquals(JSLinearString* str, + const char16_t (&chars)[NullTerminatedLength]) { + MOZ_ASSERT(NullTerminatedLength > 0); + const size_t length = NullTerminatedLength - 1; + MOZ_ASSERT(chars[length] == '\0'); + + return str->length() == length && StringStartsWith(str, chars); +} + +inline int32_t IndexOf(Handle str, char16_t target, + size_t start = 0) { + int32_t length = str->length(); + for (int32_t i = start; i < length; i++) { + if (CharAt(str, i) == target) { + return i; + } + } + + return -1; +} + +inline int32_t LastIndexOf(Handle str, char16_t target) { + int32_t length = str->length(); + for (int32_t i = length - 1; i >= 0; i--) { + if (CharAt(str, i) == target) { + return i; + } + } + + return -1; +} + +inline JSLinearString* ReplaceCharGlobally(JSContext* cx, + Handle str, + char16_t target, + char16_t replacement) { + int32_t i = IndexOf(str, target); + if (i == -1) { + return str; + } + + JS::AutoStableStringChars chars(cx); + if (!chars.initTwoByte(cx, str)) { + return nullptr; + } + + Vector buf(cx); + if (!buf.append(chars.twoByteChars(), str->length())) { + return nullptr; + } + + for (; i < int32_t(buf.length()); i++) { + if (buf[i] == target) { + buf[i] = replacement; + } + } + + RootedString result(cx, JS_NewUCStringCopyN(cx, buf.begin(), buf.length())); + if (!result) { + return nullptr; + } + + return JS_EnsureLinearString(cx, result); +} + +inline JSString* JoinStrings(JSContext* cx, + Handle> strings, + Handle separator) { + RootedString result(cx, JS_GetEmptyString(cx)); + + for (size_t i = 0; i < strings.length(); i++) { + HandleString str = strings[i]; + if (i != 0) { + result = JS_ConcatStrings(cx, result, separator); + if (!result) { + return nullptr; + } + } + + result = JS_ConcatStrings(cx, result, str); + if (!result) { + return nullptr; + } + } + + return result; +} + +} // namespace shell +} // namespace js + +#endif // shell_StringUtils_h diff --git a/js/src/shell/WasmTesting.cpp b/js/src/shell/WasmTesting.cpp new file mode 100644 index 0000000000..3d5aa3d943 --- /dev/null +++ b/js/src/shell/WasmTesting.cpp @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Copyright 2015 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "shell/WasmTesting.h" + +#include +#include +#include + +#include "js/Printf.h" +#include "wasm/WasmTypeDecls.h" + +using namespace js; +using namespace js::wasm; + +extern "C" { +bool wasm_text_to_binary(const char16_t* text, size_t text_len, + uint8_t** out_bytes, size_t* out_bytes_len, + uint8_t** out_error, size_t* out_error_len); +} // extern "C" + +bool wasm::TextToBinary(const char16_t* text, size_t textLen, Bytes* bytes, + UniqueChars* error) { + uint8_t* outBytes = nullptr; + size_t outBytesLength = 0; + + uint8_t* outError = nullptr; + size_t outErrorLength = 0; + + bool result = wasm_text_to_binary(text, textLen, &outBytes, &outBytesLength, + &outError, &outErrorLength); + + if (result) { + if (outBytesLength == 0) { + *error = JS_smprintf("missing bytes"); + return false; + } + + MOZ_ASSERT(outBytes); + MOZ_ASSERT(outBytesLength > 0); + bytes->replaceRawBuffer(outBytes, outBytesLength); + return true; + } + + MOZ_ASSERT(outError); + MOZ_ASSERT(outErrorLength > 0); + *error = UniqueChars{(char*)outError}; + return false; +} diff --git a/js/src/shell/WasmTesting.h b/js/src/shell/WasmTesting.h new file mode 100644 index 0000000000..47e2f67d9b --- /dev/null +++ b/js/src/shell/WasmTesting.h @@ -0,0 +1,37 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Copyright 2020 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef shell_wasm_h +#define shell_wasm_h + +#include "wasm/WasmTypeDecls.h" + +namespace js { +namespace wasm { + +// Translate the textual representation of a wasm module (given by a +// char16_t array + length) into serialized bytes. If there is an error +// other than out-of-memory an error message string will be stored in 'error'. + +[[nodiscard]] extern bool TextToBinary(const char16_t* text, size_t textLen, + Bytes* bytes, UniqueChars* error); + +} // namespace wasm +} // namespace js + +#endif // shell_wasm_h diff --git a/js/src/shell/fuzz-flags.txt b/js/src/shell/fuzz-flags.txt new file mode 100644 index 0000000000..0c16988fc6 --- /dev/null +++ b/js/src/shell/fuzz-flags.txt @@ -0,0 +1,97 @@ +# This lists all the possible flags we'd like to see tested out in the shell by +# fuzzers. A non-empty line not starting with # should be considered a valid +# one. Note the following flag is recommended in ALL the cases: --fuzzing-safe + +# general jit flags +--baseline-eager +--blinterp +--no-blinterp +--blinterp-eager +--cache-ir-stubs=off +--cache-ir-stubs=on +--ion-check-range-analysis +--ion-eager +--ion-edgecase-analysis=off +--ion-edgecase-analysis=on +--ion-extra-checks +--ion-gvn=off +--ion-gvn=on +--ion-inlining=off +--ion-inlining=on +--ion-instruction-reordering=off +--ion-instruction-reordering=on +--ion-licm=off +--ion-licm=on +--ion-limit-script-size=off +--ion-limit-script-size=on +--ion-offthread-compile=off +--ion-optimize-shapeguards=off +--ion-optimize-shapeguards=on +--ion-optimize-gcbarriers=off +--ion-optimize-gcbarriers=on +--ion-osr=off +--ion-osr=on +--ion-pruning=off +--ion-pruning=on +--ion-range-analysis=off +--ion-range-analysis=on +--ion-regalloc=testbed +--ion-scalar-replacement=off +--ion-scalar-replacement=on +--ion-iterator-indices=off +--ion-iterator-indices=on +--ion-warmup-threshold=0 +--ion-warmup-threshold=10 +--ion-warmup-threshold=100 +--no-native-regexp +--nursery-strings=off +--nursery-strings=on +--nursery-bigints=off +--nursery-bigints=on +--spectre-mitigations=off +--spectre-mitigations=on +--more-compartments +--fast-warmup +--no-jit-backend +--enable-watchtower +--disable-watchtower + +# GC-related +# These 2 flags can cause the shell to slow down +# --gc-zeal=2 +# --gc-zeal=10 +--no-cgc +--no-ggc +--no-incremental-gc + +# wasm flags +--wasm-gc +--wasm-compiler=baseline +--wasm-compiler=optimizing +--wasm-compiler=baseline+optimizing +--test-wasm-await-tier2 +--wasm-disable-huge-memory + +# CPU instruction set-related +--no-sse3 +--no-ssse3 +--no-sse41 +--no-sse42 + +# arm specific, no-ops on other platforms. +--arm-sim-icache-checks +--arm-asm-nop-fill=1 +--arm-hwcap=vfp + +# Profiling, code coverage, and debugging +# --dump-bytecode option implies --code-coverage +--dump-bytecode + +# Shadow Realms +--enable-shadow-realms + +# Array.fromAsync +--enable-array-from-async + +# Change Array By Copy +--enable-change-array-by-copy diff --git a/js/src/shell/js-gdb.py b/js/src/shell/js-gdb.py new file mode 100644 index 0000000000..72d73508d6 --- /dev/null +++ b/js/src/shell/js-gdb.py @@ -0,0 +1,21 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +""" GDB Python customization auto-loader for JS shell binary """ + +# This script will be installed into $objdir/dist/bin. Add $objdir to gdb's +# source search path and load in the Gecko+JS init file. + +import os +import re +from os.path import abspath, dirname + +import gdb + +devel_objdir = abspath(os.path.join(dirname(__file__), "..", "..")) +m = re.search(r"[\w ]+: (.*)", gdb.execute("show dir", False, True)) +if m and devel_objdir not in m.group(1).split(":"): + gdb.execute("set dir {}:{}".format(devel_objdir, m.group(1))) + +gdb.execute("source -s build/.gdbinit.loader") diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp new file mode 100644 index 0000000000..6c64dde263 --- /dev/null +++ b/js/src/shell/js.cpp @@ -0,0 +1,12594 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* JS shell. */ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/EnumSet.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/mozalloc.h" +#include "mozilla/PodOperations.h" +#include "mozilla/RandomNum.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Sprintf.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtrExtensions.h" // UniqueFreePtr +#include "mozilla/Utf8.h" +#include "mozilla/Variant.h" + +#include +#include +#ifdef XP_WIN +# include +# include +#endif +#include +#include +#if defined(XP_WIN) +# include /* for isatty() */ +#endif +#include +#if defined(MALLOC_H) +# include MALLOC_H /* for malloc_usable_size, malloc_size, _msize */ +#endif +#include +#include +#ifndef __wasi__ +# include +#endif +#include +#include +#include +#include +#include +#include +#ifdef XP_UNIX +# ifndef __wasi__ +# include +# include +# endif +# include +# include +#endif +#ifdef XP_LINUX +# include +#endif + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "jstypes.h" +#ifndef JS_WITHOUT_NSPR +# include "prerror.h" +# include "prlink.h" +#endif + +#include "builtin/Array.h" +#include "builtin/MapObject.h" +#include "builtin/ModuleObject.h" +#include "builtin/RegExp.h" +#include "builtin/TestingFunctions.h" +#include "builtin/TestingUtility.h" // js::ParseCompileOptions, js::ParseDebugMetadata, js::CreateScriptPrivate +#include "debugger/DebugAPI.h" +#include "frontend/BytecodeCompilation.h" +#include "frontend/BytecodeCompiler.h" +#include "frontend/CompilationStencil.h" +#ifdef JS_ENABLE_SMOOSH +# include "frontend/Frontend2.h" +#endif +#include "frontend/FrontendContext.h" // AutoReportFrontendContext +#include "frontend/ModuleSharedContext.h" +#include "frontend/Parser.h" +#include "frontend/ScopeBindingCache.h" // js::frontend::ScopeBindingCache +#include "frontend/SourceNotes.h" // SrcNote, SrcNoteType, SrcNoteIterator +#include "gc/PublicIterators.h" +#ifdef DEBUG +# include "irregexp/RegExpAPI.h" +#endif +#include "gc/GC-inl.h" // ZoneCellIter + +#ifdef JS_SIMULATOR_ARM +# include "jit/arm/Simulator-arm.h" +#endif +#ifdef JS_SIMULATOR_MIPS32 +# include "jit/mips32/Simulator-mips32.h" +#endif +#ifdef JS_SIMULATOR_MIPS64 +# include "jit/mips64/Simulator-mips64.h" +#endif +#ifdef JS_SIMULATOR_LOONG64 +# include "jit/loong64/Simulator-loong64.h" +#endif +#ifdef JS_SIMULATOR_RISCV64 +# include "jit/riscv64/Simulator-riscv64.h" +#endif +#include "jit/CacheIRHealth.h" +#include "jit/InlinableNatives.h" +#include "jit/Ion.h" +#include "jit/JitcodeMap.h" +#include "jit/shared/CodeGenerator-shared.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/ArrayBuffer.h" // JS::{CreateMappedArrayBufferContents,NewMappedArrayBufferWithContents,IsArrayBufferObject,GetArrayBufferLengthAndData} +#include "js/BuildId.h" // JS::BuildIdCharVector, JS::SetProcessBuildIdOp +#include "js/CallAndConstruct.h" // JS::Call, JS::IsCallable, JS_CallFunction, JS_CallFunctionValue +#include "js/CharacterEncoding.h" // JS::StringIsASCII +#include "js/CompilationAndEvaluation.h" +#include "js/CompileOptions.h" +#include "js/ContextOptions.h" // JS::ContextOptions{,Ref} +#include "js/Debug.h" +#include "js/Equality.h" // JS::SameValue +#include "js/ErrorReport.h" // JS::PrintError +#include "js/Exception.h" // JS::StealPendingExceptionStack +#include "js/experimental/CodeCoverage.h" // js::EnableCodeCoverage +#include "js/experimental/CTypes.h" // JS::InitCTypesClass +#include "js/experimental/Intl.h" // JS::AddMoz{DateTimeFormat,DisplayNames}Constructor +#include "js/experimental/JitInfo.h" // JSJit{Getter,Setter,Method}CallArgs, JSJitGetterInfo, JSJit{Getter,Setter}Op, JSJitInfo +#include "js/experimental/JSStencil.h" // JS::Stencil, JS::CompileToStencilOffThread, JS::FinishCompileToStencilOffThread +#include "js/experimental/SourceHook.h" // js::{Set,Forget,}SourceHook +#include "js/experimental/TypedData.h" // JS_NewUint8Array +#include "js/friend/DumpFunctions.h" // JS::FormatStackDump +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit +#include "js/friend/WindowProxy.h" // js::IsWindowProxy, js::SetWindowProxyClass, js::ToWindowProxyIfWindow, js::ToWindowIfWindowProxy +#include "js/GCAPI.h" // JS::AutoCheckCannotGC +#include "js/GCVector.h" +#include "js/GlobalObject.h" +#include "js/Initialization.h" +#include "js/Interrupt.h" +#include "js/JSON.h" +#include "js/MemoryCallbacks.h" +#include "js/MemoryFunctions.h" +#include "js/Modules.h" // JS::GetModulePrivate, JS::SetModule{DynamicImport,Metadata,Resolve}Hook, JS::SetModulePrivate +#include "js/Object.h" // JS::GetClass, JS::GetCompartment, JS::GetReservedSlot, JS::SetReservedSlot +#include "js/Principals.h" +#include "js/Printer.h" // QuoteString +#include "js/Printf.h" +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineFunction, JS_DefineFunctions, JS_DefineProperties, JS_DefineProperty, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasProperty, JS_SetElement, JS_SetProperty, JS_SetPropertyById +#include "js/PropertySpec.h" +#include "js/Realm.h" +#include "js/RegExp.h" // JS::ObjectIsRegExp +#include "js/ScriptPrivate.h" +#include "js/SourceText.h" +#include "js/StableStringChars.h" +#include "js/Stack.h" +#include "js/StreamConsumer.h" +#include "js/StructuredClone.h" +#include "js/SweepingAPI.h" +#include "js/Transcoding.h" // JS::TranscodeBuffer, JS::TranscodeRange +#include "js/Warnings.h" // JS::SetWarningReporter +#include "js/WasmModule.h" // JS::WasmModule +#include "js/Wrapper.h" +#include "proxy/DeadObjectProxy.h" // js::IsDeadProxyObject +#include "shell/jsoptparse.h" +#include "shell/jsshell.h" +#include "shell/OSObject.h" +#include "shell/ShellModuleObjectWrapper.h" +#include "shell/WasmTesting.h" +#include "threading/ConditionVariable.h" +#include "threading/ExclusiveData.h" +#include "threading/LockGuard.h" +#include "threading/Thread.h" +#include "util/CompleteFile.h" // js::FileContents, js::ReadCompleteFile +#include "util/DifferentialTesting.h" +#include "util/StringBuffer.h" +#include "util/Text.h" +#include "util/WindowsWrapper.h" +#include "vm/ArgumentsObject.h" +#include "vm/Compression.h" +#include "vm/ErrorObject.h" +#include "vm/ErrorReporting.h" +#include "vm/HelperThreads.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/JSFunction.h" +#include "vm/JSObject.h" +#include "vm/JSScript.h" +#include "vm/ModuleBuilder.h" // js::ModuleBuilder +#include "vm/Modules.h" +#include "vm/Monitor.h" +#include "vm/MutexIDs.h" +#include "vm/PromiseObject.h" // js::PromiseObject +#include "vm/Shape.h" +#include "vm/SharedArrayObject.h" +#include "vm/StencilObject.h" // js::StencilObject +#include "vm/Time.h" +#include "vm/ToSource.h" // js::ValueToSource +#include "vm/TypedArrayObject.h" +#include "vm/WrapperObject.h" +#include "wasm/WasmJS.h" + +#include "vm/Compartment-inl.h" +#include "vm/ErrorObject-inl.h" +#include "vm/Interpreter-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/Realm-inl.h" +#include "vm/Stack-inl.h" + +using namespace js; +using namespace js::cli; +using namespace js::shell; + +using JS::AutoStableStringChars; +using JS::CompileOptions; + +using js::shell::RCFile; + +using mozilla::ArrayEqual; +using mozilla::AsVariant; +using mozilla::Atomic; +using mozilla::MakeScopeExit; +using mozilla::Maybe; +using mozilla::Nothing; +using mozilla::NumberEqualsInt32; +using mozilla::TimeDuration; +using mozilla::TimeStamp; +using mozilla::Utf8Unit; +using mozilla::Variant; + +bool InitOptionParser(OptionParser& op); +bool SetGlobalOptionsPreJSInit(const OptionParser& op); +bool SetGlobalOptionsPostJSInit(const OptionParser& op); +bool SetContextOptions(JSContext* cx, const OptionParser& op); +bool SetContextWasmOptions(JSContext* cx, const OptionParser& op); +bool SetContextJITOptions(JSContext* cx, const OptionParser& op); +bool SetContextGCOptions(JSContext* cx, const OptionParser& op); +bool InitModuleLoader(JSContext* cx, const OptionParser& op); + +#ifdef FUZZING_JS_FUZZILLI +# define REPRL_CRFD 100 +# define REPRL_CWFD 101 +# define REPRL_DRFD 102 +# define REPRL_DWFD 103 + +# define SHM_SIZE 0x100000 +# define MAX_EDGES ((SHM_SIZE - 4) * 8) + +struct shmem_data { + uint32_t num_edges; + unsigned char edges[]; +}; + +struct shmem_data* __shmem; + +uint32_t *__edges_start, *__edges_stop; +void __sanitizer_cov_reset_edgeguards() { + uint64_t N = 0; + for (uint32_t* x = __edges_start; x < __edges_stop && N < MAX_EDGES; x++) + *x = ++N; +} + +extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, + uint32_t* stop) { + // Avoid duplicate initialization + if (start == stop || *start) return; + + if (__edges_start != NULL || __edges_stop != NULL) { + fprintf(stderr, + "Coverage instrumentation is only supported for a single module\n"); + _exit(-1); + } + + __edges_start = start; + __edges_stop = stop; + + // Map the shared memory region + const char* shm_key = getenv("SHM_ID"); + if (!shm_key) { + puts("[COV] no shared memory bitmap available, skipping"); + __shmem = (struct shmem_data*)malloc(SHM_SIZE); + } else { + int fd = shm_open(shm_key, O_RDWR, S_IREAD | S_IWRITE); + if (fd <= -1) { + fprintf(stderr, "Failed to open shared memory region: %s\n", + strerror(errno)); + _exit(-1); + } + + __shmem = (struct shmem_data*)mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED, fd, 0); + if (__shmem == MAP_FAILED) { + fprintf(stderr, "Failed to mmap shared memory region\n"); + _exit(-1); + } + } + + __sanitizer_cov_reset_edgeguards(); + + __shmem->num_edges = stop - start; + printf("[COV] edge counters initialized. Shared memory: %s with %u edges\n", + shm_key, __shmem->num_edges); +} + +extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t* guard) { + // There's a small race condition here: if this function executes in two + // threads for the same edge at the same time, the first thread might disable + // the edge (by setting the guard to zero) before the second thread fetches + // the guard value (and thus the index). However, our instrumentation ignores + // the first edge (see libcoverage.c) and so the race is unproblematic. + uint32_t index = *guard; + // If this function is called before coverage instrumentation is properly + // initialized we want to return early. + if (!index) return; + __shmem->edges[index / 8] |= 1 << (index % 8); + *guard = 0; +} +#endif /* FUZZING_JS_FUZZILLI */ + +enum JSShellExitCode { + EXITCODE_RUNTIME_ERROR = 3, + EXITCODE_FILE_NOT_FOUND = 4, + EXITCODE_OUT_OF_MEMORY = 5, + EXITCODE_TIMEOUT = 6 +}; + +/* + * Limit the timeout to 30 minutes to prevent an overflow on platfoms + * that represent the time internally in microseconds using 32-bit int. + */ +static const double MAX_TIMEOUT_SECONDS = 1800.0; + +// Not necessarily in sync with the browser +#ifdef ENABLE_SHARED_MEMORY +# define SHARED_MEMORY_DEFAULT 1 +#else +# define SHARED_MEMORY_DEFAULT 0 +#endif + +// Fuzzing support for JS runtime fuzzing +#ifdef FUZZING_INTERFACES +# include "shell/jsrtfuzzing/jsrtfuzzing.h" +static bool fuzzDoDebug = !!getenv("MOZ_FUZZ_DEBUG"); +static bool fuzzHaveModule = !!getenv("FUZZER"); +#endif // FUZZING_INTERFACES + +// Code to support GCOV code coverage measurements on standalone shell +#ifdef MOZ_CODE_COVERAGE +# if defined(__GNUC__) && !defined(__clang__) +extern "C" void __gcov_dump(); +extern "C" void __gcov_reset(); + +void counters_dump(int) { __gcov_dump(); } + +void counters_reset(int) { __gcov_reset(); } +# else +void counters_dump(int) { /* Do nothing */ +} + +void counters_reset(int) { /* Do nothing */ +} +# endif + +static void InstallCoverageSignalHandlers() { +# ifndef XP_WIN + fprintf(stderr, "[CodeCoverage] Setting handlers for process %d.\n", + getpid()); + + struct sigaction dump_sa; + dump_sa.sa_handler = counters_dump; + dump_sa.sa_flags = SA_RESTART; + sigemptyset(&dump_sa.sa_mask); + mozilla::DebugOnly r1 = sigaction(SIGUSR1, &dump_sa, nullptr); + MOZ_ASSERT(r1 == 0, "Failed to install GCOV SIGUSR1 handler"); + + struct sigaction reset_sa; + reset_sa.sa_handler = counters_reset; + reset_sa.sa_flags = SA_RESTART; + sigemptyset(&reset_sa.sa_mask); + mozilla::DebugOnly r2 = sigaction(SIGUSR2, &reset_sa, nullptr); + MOZ_ASSERT(r2 == 0, "Failed to install GCOV SIGUSR2 handler"); +# endif +} +#endif + +// An off-thread parse or decode job. +class js::shell::OffThreadJob { + enum State { + RUNNING, // Working; no token. + DONE, // Finished; have token. + CANCELLED // Cancelled due to error. + }; + + public: + using Source = mozilla::Variant; + + OffThreadJob(ShellContext* sc, Source&& source); + ~OffThreadJob(); + + void cancel(); + void markDone(JS::OffThreadToken* newToken); + JS::OffThreadToken* waitUntilDone(JSContext* cx); + + char16_t* sourceChars() { return source.as().get(); } + JS::TranscodeBuffer& xdrBuffer() { return source.as(); } + + public: + const int32_t id; + + private: + js::Monitor& monitor; + State state; + JS::OffThreadToken* token; + Source source; +}; + +static OffThreadJob* NewOffThreadJob(JSContext* cx, CompileOptions& options, + OffThreadJob::Source&& source) { + ShellContext* sc = GetShellContext(cx); + UniquePtr job(cx->new_(sc, std::move(source))); + if (!job) { + return nullptr; + } + + if (!sc->offThreadJobs.append(job.get())) { + job->cancel(); + JS_ReportErrorASCII(cx, "OOM adding off-thread job"); + return nullptr; + } + + return job.release(); +} + +static OffThreadJob* GetSingleOffThreadJob(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + const auto& jobs = sc->offThreadJobs; + if (jobs.empty()) { + JS_ReportErrorASCII(cx, "No off-thread jobs are pending"); + return nullptr; + } + + if (jobs.length() > 1) { + JS_ReportErrorASCII( + cx, "Multiple off-thread jobs are pending: must specify job ID"); + return nullptr; + } + + return jobs[0]; +} + +static OffThreadJob* LookupOffThreadJobByID(JSContext* cx, int32_t id) { + if (id <= 0) { + JS_ReportErrorASCII(cx, "Bad off-thread job ID"); + return nullptr; + } + + ShellContext* sc = GetShellContext(cx); + const auto& jobs = sc->offThreadJobs; + if (jobs.empty()) { + JS_ReportErrorASCII(cx, "No off-thread jobs are pending"); + return nullptr; + } + + OffThreadJob* job = nullptr; + for (auto someJob : jobs) { + if (someJob->id == id) { + job = someJob; + break; + } + } + + if (!job) { + JS_ReportErrorASCII(cx, "Off-thread job not found"); + return nullptr; + } + + return job; +} + +static OffThreadJob* LookupOffThreadJobForArgs(JSContext* cx, + const CallArgs& args, + size_t arg) { + // If the optional ID argument isn't present, get the single pending job. + if (args.length() <= arg) { + return GetSingleOffThreadJob(cx); + } + + // Lookup the job using the specified ID. + int32_t id = 0; + RootedValue value(cx, args[arg]); + if (!ToInt32(cx, value, &id)) { + return nullptr; + } + + return LookupOffThreadJobByID(cx, id); +} + +static void DeleteOffThreadJob(JSContext* cx, OffThreadJob* job) { + ShellContext* sc = GetShellContext(cx); + for (size_t i = 0; i < sc->offThreadJobs.length(); i++) { + if (sc->offThreadJobs[i] == job) { + sc->offThreadJobs.erase(&sc->offThreadJobs[i]); + js_delete(job); + return; + } + } + + MOZ_CRASH("Off-thread job not found"); +} + +static void CancelOffThreadJobsForContext(JSContext* cx) { + // Parse jobs may be blocked waiting on GC. + gc::FinishGC(cx); + + // Wait for jobs belonging to this context. + ShellContext* sc = GetShellContext(cx); + while (!sc->offThreadJobs.empty()) { + OffThreadJob* job = sc->offThreadJobs.popCopy(); + job->waitUntilDone(cx); + js_delete(job); + } +} + +static void CancelOffThreadJobsForRuntime(JSContext* cx) { + // Parse jobs may be blocked waiting on GC. + gc::FinishGC(cx); + + // Cancel jobs belonging to this runtime. + CancelOffThreadParses(cx->runtime()); + ShellContext* sc = GetShellContext(cx); + while (!sc->offThreadJobs.empty()) { + js_delete(sc->offThreadJobs.popCopy()); + } +} + +mozilla::Atomic gOffThreadJobSerial(1); + +OffThreadJob::OffThreadJob(ShellContext* sc, Source&& source) + : id(gOffThreadJobSerial++), + monitor(sc->offThreadMonitor), + state(RUNNING), + token(nullptr), + source(std::move(source)) { + MOZ_RELEASE_ASSERT(id > 0, "Off-thread job IDs exhausted"); +} + +OffThreadJob::~OffThreadJob() { MOZ_ASSERT(state != RUNNING); } + +void OffThreadJob::cancel() { + MOZ_ASSERT(state == RUNNING); + MOZ_ASSERT(!token); + + state = CANCELLED; +} + +void OffThreadJob::markDone(JS::OffThreadToken* newToken) { + AutoLockMonitor alm(monitor); + MOZ_ASSERT(state == RUNNING); + MOZ_ASSERT(!token); + MOZ_ASSERT(newToken); + + token = newToken; + state = DONE; + alm.notifyAll(); +} + +JS::OffThreadToken* OffThreadJob::waitUntilDone(JSContext* cx) { + AutoLockMonitor alm(monitor); + MOZ_ASSERT(state != CANCELLED); + + while (state != DONE) { + alm.wait(); + } + + MOZ_ASSERT(token); + return token; +} + +struct ShellCompartmentPrivate { + GCPtr blackRoot; + GCPtr grayRoot; +}; + +struct MOZ_STACK_CLASS EnvironmentPreparer + : public js::ScriptEnvironmentPreparer { + explicit EnvironmentPreparer(JSContext* cx) { + js::SetScriptEnvironmentPreparer(cx, this); + } + void invoke(JS::HandleObject global, Closure& closure) override; +}; + +const char* shell::selfHostedXDRPath = nullptr; +bool shell::encodeSelfHostedCode = false; +bool shell::enableCodeCoverage = false; +bool shell::enableDisassemblyDumps = false; +bool shell::offthreadCompilation = false; +JS::DelazificationOption shell::defaultDelazificationMode = + JS::DelazificationOption::OnDemandOnly; +bool shell::enableAsmJS = false; +bool shell::enableWasm = false; +bool shell::enableSharedMemory = SHARED_MEMORY_DEFAULT; +bool shell::enableWasmBaseline = false; +bool shell::enableWasmOptimizing = false; + +#define WASM_DEFAULT_FEATURE(NAME, ...) bool shell::enableWasm##NAME = true; +#define WASM_EXPERIMENTAL_FEATURE(NAME, ...) \ + bool shell::enableWasm##NAME = false; +JS_FOR_WASM_FEATURES(WASM_DEFAULT_FEATURE, WASM_DEFAULT_FEATURE, + WASM_EXPERIMENTAL_FEATURE); +#undef WASM_DEFAULT_FEATURE +#undef WASM_EXPERIMENTAL_FEATURE + +bool shell::enableWasmVerbose = false; +bool shell::enableTestWasmAwaitTier2 = false; +bool shell::enableSourcePragmas = true; +bool shell::enableAsyncStacks = false; +bool shell::enableAsyncStackCaptureDebuggeeOnly = false; +bool shell::enableWeakRefs = false; +bool shell::enableToSource = false; +bool shell::enablePropertyErrorMessageFix = false; +bool shell::enableIteratorHelpers = false; +bool shell::enableShadowRealms = false; +bool shell::enableArrayFromAsync = true; +#ifdef NIGHTLY_BUILD +bool shell::enableArrayGrouping = false; +// Pref for String.prototype.{is,to}WellFormed() methods. +bool shell::enableWellFormedUnicodeStrings = false; +#endif +bool shell::enableChangeArrayByCopy = false; +#ifdef ENABLE_NEW_SET_METHODS +bool shell::enableNewSetMethods = true; +#endif +bool shell::enableImportAssertions = false; +#ifdef JS_GC_ZEAL +uint32_t shell::gZealBits = 0; +uint32_t shell::gZealFrequency = 0; +#endif +bool shell::printTiming = false; +RCFile* shell::gErrFile = nullptr; +RCFile* shell::gOutFile = nullptr; +bool shell::reportWarnings = true; +bool shell::compileOnly = false; +bool shell::disableOOMFunctions = false; +bool shell::defaultToSameCompartment = true; + +bool shell::useFdlibmForSinCosTan = false; + +#ifdef DEBUG +bool shell::dumpEntrainedVariables = false; +bool shell::OOM_printAllocationCount = false; +#endif + +UniqueChars shell::processWideModuleLoadPath; + +static bool SetTimeoutValue(JSContext* cx, double t); + +static void KillWatchdog(JSContext* cx); + +static bool ScheduleWatchdog(JSContext* cx, double t); + +static void CancelExecution(JSContext* cx); + +enum class ShellGlobalKind { + GlobalObject, + WindowProxy, +}; + +static JSObject* NewGlobalObject(JSContext* cx, JS::RealmOptions& options, + JSPrincipals* principals, ShellGlobalKind kind, + bool immutablePrototype); + +/* + * A toy WindowProxy class for the shell. This is intended for testing code + * where global |this| is a WindowProxy. All requests are forwarded to the + * underlying global and no navigation is supported. + */ +const JSClass ShellWindowProxyClass = + PROXY_CLASS_DEF("ShellWindowProxy", JSCLASS_HAS_RESERVED_SLOTS(1)); + +JSObject* NewShellWindowProxy(JSContext* cx, JS::HandleObject global) { + MOZ_ASSERT(global->is()); + + js::WrapperOptions options; + options.setClass(&ShellWindowProxyClass); + + JSAutoRealm ar(cx, global); + JSObject* obj = + js::Wrapper::New(cx, global, &js::Wrapper::singleton, options); + MOZ_ASSERT_IF(obj, js::IsWindowProxy(obj)); + return obj; +} + +/* + * A toy principals type for the shell. + * + * In the shell, a principal is simply a 32-bit mask: P subsumes Q if the + * set bits in P are a superset of those in Q. Thus, the principal 0 is + * subsumed by everything, and the principal ~0 subsumes everything. + * + * As a special case, a null pointer as a principal is treated like 0xffff. + * + * The 'newGlobal' function takes an option indicating which principal the + * new global should have; 'evaluate' does for the new code. + */ +class ShellPrincipals final : public JSPrincipals { + uint32_t bits; + + static uint32_t getBits(JSPrincipals* p) { + if (!p) { + return 0xffff; + } + return static_cast(p)->bits; + } + + public: + explicit ShellPrincipals(uint32_t bits, int32_t refcount = 0) : bits(bits) { + this->refcount = refcount; + } + + bool write(JSContext* cx, JSStructuredCloneWriter* writer) override { + // The shell doesn't have a read principals hook, so it doesn't really + // matter what we write here, but we have to write something so the + // fuzzer is happy. + return JS_WriteUint32Pair(writer, bits, 0); + } + + bool isSystemOrAddonPrincipal() override { return true; } + + static void destroy(JSPrincipals* principals) { + MOZ_ASSERT(principals != &fullyTrusted); + MOZ_ASSERT(principals->refcount == 0); + js_delete(static_cast(principals)); + } + + static bool subsumes(JSPrincipals* first, JSPrincipals* second) { + uint32_t firstBits = getBits(first); + uint32_t secondBits = getBits(second); + return (firstBits | secondBits) == firstBits; + } + + static JSSecurityCallbacks securityCallbacks; + + // Fully-trusted principals singleton. + static ShellPrincipals fullyTrusted; +}; + +JSSecurityCallbacks ShellPrincipals::securityCallbacks = { + nullptr, // contentSecurityPolicyAllows + subsumes}; + +// The fully-trusted principal subsumes all other principals. +ShellPrincipals ShellPrincipals::fullyTrusted(-1, 1); + +#ifdef EDITLINE +extern "C" { +extern MOZ_EXPORT char* readline(const char* prompt); +extern MOZ_EXPORT void add_history(char* line); +} // extern "C" +#endif + +ShellContext::ShellContext(JSContext* cx) + : isWorker(false), + lastWarningEnabled(false), + trackUnhandledRejections(true), + timeoutInterval(-1.0), + startTime(PRMJ_Now()), + serviceInterrupt(false), + haveInterruptFunc(false), + interruptFunc(cx, NullValue()), + lastWarning(cx, NullValue()), + promiseRejectionTrackerCallback(cx, NullValue()), + unhandledRejectedPromises(cx), + watchdogLock(mutexid::ShellContextWatchdog), + exitCode(0), + quitting(false), + readLineBufPos(0), + errFilePtr(nullptr), + outFilePtr(nullptr), + offThreadMonitor(mutexid::ShellOffThreadState), + finalizationRegistryCleanupCallbacks(cx) {} + +ShellContext::~ShellContext() { MOZ_ASSERT(offThreadJobs.empty()); } + +ShellContext* js::shell::GetShellContext(JSContext* cx) { + ShellContext* sc = static_cast(JS_GetContextPrivate(cx)); + MOZ_ASSERT(sc); + return sc; +} + +static void TraceRootArrays(JSTracer* trc, gc::MarkColor color) { + JSRuntime* rt = trc->runtime(); + for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) { + for (CompartmentsInZoneIter comp(zone); !comp.done(); comp.next()) { + auto priv = static_cast( + JS_GetCompartmentPrivate(comp.get())); + if (!priv) { + continue; + } + + GCPtr& array = + (color == gc::MarkColor::Black) ? priv->blackRoot : priv->grayRoot; + TraceNullableEdge(trc, &array, "shell root array"); + + if (array) { + // Trace the array elements as part of root marking. + for (uint32_t i = 0; i < array->getDenseInitializedLength(); i++) { + Value& value = const_cast(array->getDenseElement(i)); + TraceManuallyBarrieredEdge(trc, &value, "shell root array element"); + } + } + } + } +} + +static void TraceBlackRoots(JSTracer* trc, void* data) { + TraceRootArrays(trc, gc::MarkColor::Black); +} + +static bool TraceGrayRoots(JSTracer* trc, SliceBudget& budget, void* data) { + TraceRootArrays(trc, gc::MarkColor::Gray); + return true; +} + +static inline JSString* NewStringCopyUTF8(JSContext* cx, const char* chars) { + return JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(chars, strlen(chars))); +} + +static mozilla::UniqueFreePtr GetLine(FILE* file, const char* prompt) { +#ifdef EDITLINE + /* + * Use readline only if file is stdin, because there's no way to specify + * another handle. Are other filehandles interactive? + */ + if (file == stdin) { + mozilla::UniqueFreePtr linep(readline(prompt)); + /* + * We set it to zero to avoid complaining about inappropriate ioctl + * for device in the case of EOF. Looks like errno == 251 if line is + * finished with EOF and errno == 25 (EINVAL on Mac) if there is + * nothing left to read. + */ + if (errno == 251 || errno == 25 || errno == EINVAL) { + errno = 0; + } + if (!linep) { + return nullptr; + } + if (linep[0] != '\0') { + add_history(linep.get()); + } + return linep; + } +#endif + + size_t len = 0; + if (*prompt != '\0' && gOutFile->isOpen()) { + fprintf(gOutFile->fp, "%s", prompt); + fflush(gOutFile->fp); + } + + size_t size = 80; + mozilla::UniqueFreePtr buffer(static_cast(malloc(size))); + if (!buffer) { + return nullptr; + } + + char* current = buffer.get(); + do { + while (true) { + if (fgets(current, size - len, file)) { + break; + } + if (errno != EINTR) { + return nullptr; + } + } + + len += strlen(current); + char* t = buffer.get() + len - 1; + if (*t == '\n') { + /* Line was read. We remove '\n' and exit. */ + *t = '\0'; + break; + } + + if (len + 1 == size) { + size = size * 2; + char* raw = buffer.release(); + char* tmp = static_cast(realloc(raw, size)); + if (!tmp) { + free(raw); + return nullptr; + } + buffer.reset(tmp); + } + current = buffer.get() + len; + } while (true); + return buffer; +} + +static bool ShellInterruptCallback(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + if (!sc->serviceInterrupt) { + return true; + } + + // Reset serviceInterrupt. CancelExecution or InterruptIf will set it to + // true to distinguish watchdog or user triggered interrupts. + // Do this first to prevent other interrupts that may occur while the + // user-supplied callback is executing from re-entering the handler. + sc->serviceInterrupt = false; + + bool result; + if (sc->haveInterruptFunc) { + bool wasAlreadyThrowing = cx->isExceptionPending(); + JS::AutoSaveExceptionState savedExc(cx); + JSAutoRealm ar(cx, &sc->interruptFunc.toObject()); + RootedValue rval(cx); + + // Report any exceptions thrown by the JS interrupt callback, but do + // *not* keep it on the cx. The interrupt handler is invoked at points + // that are not expected to throw catchable exceptions, like at + // JSOp::RetRval. + // + // If the interrupted JS code was already throwing, any exceptions + // thrown by the interrupt handler are silently swallowed. + { + Maybe are; + if (!wasAlreadyThrowing) { + are.emplace(cx); + } + result = JS_CallFunctionValue(cx, nullptr, sc->interruptFunc, + JS::HandleValueArray::empty(), &rval); + } + savedExc.restore(); + + if (rval.isBoolean()) { + result = rval.toBoolean(); + } else { + result = false; + } + } else { + result = false; + } + + if (!result && sc->exitCode == 0) { + static const char msg[] = "Script terminated by interrupt handler.\n"; + fputs(msg, stderr); + + sc->exitCode = EXITCODE_TIMEOUT; + } + + return result; +} + +static void GCSliceCallback(JSContext* cx, JS::GCProgress progress, + const JS::GCDescription& desc) { + if (progress == JS::GC_CYCLE_END) { +#if defined(MOZ_MEMORY) + // We call this here to match the browser's DOMGCSliceCallback. + jemalloc_free_dirty_pages(); +#endif + } +} + +/* + * Some UTF-8 files, notably those written using Notepad, have a Unicode + * Byte-Order-Mark (BOM) as their first character. This is useless (byte-order + * is meaningless for UTF-8) but causes a syntax error unless we skip it. + */ +static void SkipUTF8BOM(FILE* file) { + int ch1 = fgetc(file); + int ch2 = fgetc(file); + int ch3 = fgetc(file); + + // Skip the BOM + if (ch1 == 0xEF && ch2 == 0xBB && ch3 == 0xBF) { + return; + } + + // No BOM - revert + if (ch3 != EOF) { + ungetc(ch3, file); + } + if (ch2 != EOF) { + ungetc(ch2, file); + } + if (ch1 != EOF) { + ungetc(ch1, file); + } +} + +void EnvironmentPreparer::invoke(HandleObject global, Closure& closure) { + MOZ_ASSERT(JS_IsGlobalObject(global)); + + JSContext* cx = TlsContext.get(); + MOZ_ASSERT(!JS_IsExceptionPending(cx)); + + AutoRealm ar(cx, global); + AutoReportException are(cx); + if (!closure(cx)) { + return; + } +} + +static bool RegisterScriptPathWithModuleLoader(JSContext* cx, + HandleScript script, + const char* filename) { + // Set the private value associated with a script to a object containing the + // script's filename so that the module loader can use it to resolve + // relative imports. + + RootedString path(cx, NewStringCopyUTF8(cx, filename)); + if (!path) { + return false; + } + + MOZ_ASSERT(JS::GetScriptPrivate(script).isUndefined()); + RootedObject infoObject(cx, js::CreateScriptPrivate(cx, path)); + if (!infoObject) { + return false; + } + + JS::SetScriptPrivate(script, ObjectValue(*infoObject)); + return true; +} + +enum class CompileUtf8 { + InflateToUtf16, + DontInflate, +}; + +[[nodiscard]] static bool RunFile(JSContext* cx, const char* filename, + FILE* file, CompileUtf8 compileMethod, + bool compileOnly, bool fullParse) { + SkipUTF8BOM(file); + + int64_t t1 = PRMJ_Now(); + RootedScript script(cx); + + { + CompileOptions options(cx); + options.setIntroductionType("js shell file") + .setFileAndLine(filename, 1) + .setIsRunOnce(true) + .setNoScriptRval(true); + + if (fullParse) { + options.setForceFullParse(); + } else { + options.setEagerDelazificationStrategy(defaultDelazificationMode); + } + + if (compileMethod == CompileUtf8::DontInflate) { + script = JS::CompileUtf8File(cx, options, file); + } else { + fprintf(stderr, "(compiling '%s' after inflating to UTF-16)\n", filename); + + FileContents buffer(cx); + if (!ReadCompleteFile(cx, file, buffer)) { + return false; + } + + size_t length = buffer.length(); + auto chars = UniqueTwoByteChars( + UTF8CharsToNewTwoByteCharsZ( + cx, + JS::UTF8Chars(reinterpret_cast(buffer.begin()), + buffer.length()), + &length, js::MallocArena) + .get()); + if (!chars) { + return false; + } + + JS::SourceText source; + if (!source.init(cx, std::move(chars), length)) { + return false; + } + + script = JS::Compile(cx, options, source); + } + + if (!script) { + return false; + } + } + + if (!RegisterScriptPathWithModuleLoader(cx, script, filename)) { + return false; + } + +#ifdef DEBUG + if (dumpEntrainedVariables) { + AnalyzeEntrainedVariables(cx, script); + } +#endif + if (!compileOnly) { + if (!JS_ExecuteScript(cx, script)) { + return false; + } + int64_t t2 = PRMJ_Now() - t1; + if (printTiming) { + printf("runtime = %.3f ms\n", double(t2) / PRMJ_USEC_PER_MSEC); + } + } + return true; +} + +[[nodiscard]] static bool RunModule(JSContext* cx, const char* filename, + bool compileOnly) { + ShellContext* sc = GetShellContext(cx); + + RootedString path(cx, NewStringCopyUTF8(cx, filename)); + if (!path) { + return false; + } + + path = ResolvePath(cx, path, RootRelative); + if (!path) { + return false; + } + + return sc->moduleLoader->loadRootModule(cx, path); +} + +static void ShellCleanupFinalizationRegistryCallback(JSFunction* doCleanup, + JSObject* incumbentGlobal, + void* data) { + // In the browser this queues a task. Shell jobs correspond to microtasks so + // we arrange for cleanup to happen after all jobs/microtasks have run. The + // incumbent global is ignored in the shell. + + auto sc = static_cast(data); + AutoEnterOOMUnsafeRegion oomUnsafe; + if (!sc->finalizationRegistryCleanupCallbacks.append(doCleanup)) { + oomUnsafe.crash("ShellCleanupFinalizationRegistryCallback"); + } +} + +// Run any FinalizationRegistry cleanup tasks and return whether any ran. +static bool MaybeRunFinalizationRegistryCleanupTasks(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + MOZ_ASSERT(!sc->quitting); + + Rooted callbacks(cx); + std::swap(callbacks.get(), sc->finalizationRegistryCleanupCallbacks.get()); + + bool ranTasks = false; + + RootedFunction callback(cx); + for (JSFunction* f : callbacks) { + callback = f; + + JS::ExposeObjectToActiveJS(callback); + AutoRealm ar(cx, callback); + + { + AutoReportException are(cx); + RootedValue unused(cx); + (void)JS_CallFunction(cx, nullptr, callback, HandleValueArray::empty(), + &unused); + } + + ranTasks = true; + + if (sc->quitting) { + break; + } + } + + return ranTasks; +} + +static bool EnqueueJob(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!IsFunctionObject(args.get(0))) { + JS_ReportErrorASCII(cx, "EnqueueJob's first argument must be a function"); + return false; + } + + args.rval().setUndefined(); + + RootedObject job(cx, &args[0].toObject()); + return js::EnqueueJob(cx, job); +} + +static void RunShellJobs(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + if (sc->quitting) { + return; + } + + while (true) { + // Run microtasks. + js::RunJobs(cx); + if (sc->quitting) { + return; + } + + // Run tasks (only finalization registry clean tasks are possible). + bool ranTasks = MaybeRunFinalizationRegistryCleanupTasks(cx); + if (!ranTasks) { + break; + } + } +} + +static bool DrainJobQueue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (GetShellContext(cx)->quitting) { + JS_ReportErrorASCII( + cx, "Mustn't drain the job queue when the shell is quitting"); + return false; + } + + RunShellJobs(cx); + + if (GetShellContext(cx)->quitting) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool GlobalOfFirstJobInQueue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject job(cx, cx->internalJobQueue->maybeFront()); + if (!job) { + JS_ReportErrorASCII(cx, "Job queue is empty"); + return false; + } + + RootedObject global(cx, &job->nonCCWGlobal()); + if (!cx->compartment()->wrap(cx, &global)) { + return false; + } + + args.rval().setObject(*global); + return true; +} + +static bool TrackUnhandledRejections(JSContext* cx, JS::HandleObject promise, + JS::PromiseRejectionHandlingState state) { + ShellContext* sc = GetShellContext(cx); + if (!sc->trackUnhandledRejections) { + return true; + } + +#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) + if (cx->runningOOMTest) { + // When OOM happens, we cannot reliably track the set of unhandled + // promise rejections. Throw error only when simulated OOM is used + // *and* promises are used in the test. + JS_ReportErrorASCII( + cx, + "Can't track unhandled rejections while running simulated OOM " + "test. Call ignoreUnhandledRejections before using oomTest etc."); + return false; + } +#endif + + if (!sc->unhandledRejectedPromises) { + sc->unhandledRejectedPromises = SetObject::create(cx); + if (!sc->unhandledRejectedPromises) { + return false; + } + } + + RootedValue promiseVal(cx, ObjectValue(*promise)); + + AutoRealm ar(cx, sc->unhandledRejectedPromises); + if (!cx->compartment()->wrap(cx, &promiseVal)) { + return false; + } + + switch (state) { + case JS::PromiseRejectionHandlingState::Unhandled: + if (!SetObject::add(cx, sc->unhandledRejectedPromises, promiseVal)) { + return false; + } + break; + case JS::PromiseRejectionHandlingState::Handled: + bool deleted = false; + if (!SetObject::delete_(cx, sc->unhandledRejectedPromises, promiseVal, + &deleted)) { + return false; + } + // We can't MOZ_ASSERT(deleted) here, because it's possible we failed to + // add the promise in the first place, due to OOM. + break; + } + + return true; +} + +static void ForwardingPromiseRejectionTrackerCallback( + JSContext* cx, bool mutedErrors, JS::HandleObject promise, + JS::PromiseRejectionHandlingState state, void* data) { + AutoReportException are(cx); + + if (!TrackUnhandledRejections(cx, promise, state)) { + return; + } + + RootedValue callback(cx, + GetShellContext(cx)->promiseRejectionTrackerCallback); + if (callback.isNull()) { + return; + } + + AutoRealm ar(cx, &callback.toObject()); + + FixedInvokeArgs<2> args(cx); + args[0].setObject(*promise); + args[1].setInt32(static_cast(state)); + + if (!JS_WrapValue(cx, args[0])) { + return; + } + + RootedValue rval(cx); + (void)Call(cx, callback, UndefinedHandleValue, args, &rval); +} + +static bool SetPromiseRejectionTrackerCallback(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!IsFunctionObject(args.get(0))) { + JS_ReportErrorASCII( + cx, + "setPromiseRejectionTrackerCallback expects a function as its sole " + "argument"); + return false; + } + + GetShellContext(cx)->promiseRejectionTrackerCallback = args[0]; + + args.rval().setUndefined(); + return true; +} + +// clang-format off +static const char* telemetryNames[static_cast(JSMetric::Count)] = { +#define LIT(NAME, _) #NAME, + FOR_EACH_JS_METRIC(LIT) +#undef LIT +}; +// clang-format on + +// Telemetry can be executed from multiple threads, and the callback is +// responsible to avoid contention on the recorded telemetry data. +static Mutex* telemetryLock = nullptr; +class MOZ_RAII AutoLockTelemetry : public LockGuard { + using Base = LockGuard; + + public: + AutoLockTelemetry() : Base(*telemetryLock) { MOZ_ASSERT(telemetryLock); } +}; + +using TelemetryData = uint32_t; +using TelemetryVec = Vector; +static mozilla::Array telemetryResults; +static void AccumulateTelemetryDataCallback(JSMetric id, uint32_t sample) { + AutoLockTelemetry alt; + // We ignore OOMs while writting teleemtry data. + if (telemetryResults[static_cast(id)].append(sample)) { + return; + } +} + +static void WriteTelemetryDataToDisk(const char* dir) { + const int pathLen = 260; + char fileName[pathLen]; + Fprinter output; + auto initOutput = [&](const char* name) -> bool { + if (SprintfLiteral(fileName, "%s%s.csv", dir, name) >= pathLen) { + return false; + } + FILE* file = fopen(fileName, "a"); + if (!file) { + return false; + } + output.init(file); + return true; + }; + + for (size_t id = 0; id < size_t(JSMetric::Count); id++) { + auto clear = MakeScopeExit([&] { telemetryResults[id].clearAndFree(); }); + if (!initOutput(telemetryNames[id])) { + continue; + } + for (uint32_t data : telemetryResults[id]) { + output.printf("%u\n", data); + } + output.finish(); + } +} + +#undef MAP_TELEMETRY + +static bool BoundToAsyncStack(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedValue function(cx, GetFunctionNativeReserved(&args.callee(), 0)); + RootedObject options( + cx, &GetFunctionNativeReserved(&args.callee(), 1).toObject()); + + Rooted stack(cx, nullptr); + bool isExplicit; + + RootedValue v(cx); + + if (!JS_GetProperty(cx, options, "stack", &v)) { + return false; + } + if (!v.isObject() || !v.toObject().is()) { + JS_ReportErrorASCII(cx, + "The 'stack' property must be a SavedFrame object."); + return false; + } + stack = &v.toObject().as(); + + if (!JS_GetProperty(cx, options, "cause", &v)) { + return false; + } + RootedString causeString(cx, ToString(cx, v)); + if (!causeString) { + MOZ_ASSERT(cx->isExceptionPending()); + return false; + } + + UniqueChars cause = JS_EncodeStringToUTF8(cx, causeString); + if (!cause) { + MOZ_ASSERT(cx->isExceptionPending()); + return false; + } + + if (!JS_GetProperty(cx, options, "explicit", &v)) { + return false; + } + isExplicit = v.isUndefined() ? true : ToBoolean(v); + + auto kind = + (isExplicit ? JS::AutoSetAsyncStackForNewCalls::AsyncCallKind::EXPLICIT + : JS::AutoSetAsyncStackForNewCalls::AsyncCallKind::IMPLICIT); + + JS::AutoSetAsyncStackForNewCalls asasfnckthxbye(cx, stack, cause.get(), kind); + return Call(cx, UndefinedHandleValue, function, JS::HandleValueArray::empty(), + args.rval()); +} + +static bool BindToAsyncStack(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 2) { + JS_ReportErrorASCII(cx, "bindToAsyncStack takes exactly two arguments."); + return false; + } + + if (!args[0].isObject() || !IsCallable(args[0])) { + JS_ReportErrorASCII( + cx, "bindToAsyncStack's first argument should be a function."); + return false; + } + + if (!args[1].isObject()) { + JS_ReportErrorASCII( + cx, "bindToAsyncStack's second argument should be an object."); + return false; + } + + RootedFunction bound(cx, NewFunctionWithReserved(cx, BoundToAsyncStack, 0, 0, + "bindToAsyncStack thunk")); + if (!bound) { + return false; + } + SetFunctionNativeReserved(bound, 0, args[0]); + SetFunctionNativeReserved(bound, 1, args[1]); + + args.rval().setObject(*bound); + return true; +} + +#ifdef JS_HAS_INTL_API +static bool AddIntlExtras(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "addIntlExtras must be passed an object"); + return false; + } + JS::RootedObject intl(cx, &args[0].toObject()); + + static const JSFunctionSpec funcs[] = { + JS_SELF_HOSTED_FN("getCalendarInfo", "Intl_getCalendarInfo", 1, 0), + JS_FS_END}; + + if (!JS_DefineFunctions(cx, intl, funcs)) { + return false; + } + + if (!JS::AddMozDateTimeFormatConstructor(cx, intl)) { + return false; + } + + if (!JS::AddMozDisplayNamesConstructor(cx, intl)) { + return false; + } + + args.rval().setUndefined(); + return true; +} +#endif // JS_HAS_INTL_API + +[[nodiscard]] static bool EvalUtf8AndPrint(JSContext* cx, const char* bytes, + size_t length, int lineno, + bool compileOnly) { + // Eval. + JS::CompileOptions options(cx); + options.setIntroductionType("js shell interactive") + .setIsRunOnce(true) + .setFileAndLine("typein", lineno) + .setEagerDelazificationStrategy(defaultDelazificationMode); + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, bytes, length, JS::SourceOwnership::Borrowed)) { + return false; + } + + RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + if (!script) { + return false; + } + if (compileOnly) { + return true; + } + RootedValue result(cx); + if (!JS_ExecuteScript(cx, script, &result)) { + return false; + } + + if (!result.isUndefined() && gOutFile->isOpen()) { + // Print. + RootedString str(cx, JS_ValueToSource(cx, result)); + if (!str) { + return false; + } + + UniqueChars utf8chars = JS_EncodeStringToUTF8(cx, str); + if (!utf8chars) { + return false; + } + fprintf(gOutFile->fp, "%s\n", utf8chars.get()); + } + return true; +} + +[[nodiscard]] static bool ReadEvalPrintLoop(JSContext* cx, FILE* in, + bool compileOnly) { + ShellContext* sc = GetShellContext(cx); + int lineno = 1; + bool hitEOF = false; + + do { + /* + * Accumulate lines until we get a 'compilable unit' - one that either + * generates an error (before running out of source) or that compiles + * cleanly. This should be whenever we get a complete statement that + * coincides with the end of a line. + */ + int startline = lineno; + typedef Vector CharBuffer; + RootedObject globalLexical(cx, &cx->global()->lexicalEnvironment()); + CharBuffer buffer(cx); + do { + ScheduleWatchdog(cx, -1); + sc->serviceInterrupt = false; + errno = 0; + + mozilla::UniqueFreePtr line = + GetLine(in, startline == lineno ? "js> " : ""); + if (!line) { + if (errno) { + if (UniqueChars error = SystemErrorMessage(cx, errno)) { + JS_ReportErrorUTF8(cx, "%s", error.get()); + } + return false; + } + hitEOF = true; + break; + } + + if (!buffer.append(line.get(), strlen(line.get())) || + !buffer.append('\n')) { + return false; + } + + lineno++; + if (!ScheduleWatchdog(cx, sc->timeoutInterval)) { + hitEOF = true; + break; + } + } while (!JS_Utf8BufferIsCompilableUnit(cx, cx->global(), buffer.begin(), + buffer.length())); + + if (hitEOF && buffer.empty()) { + break; + } + + { + // Report exceptions but keep going. + AutoReportException are(cx); + (void)EvalUtf8AndPrint(cx, buffer.begin(), buffer.length(), startline, + compileOnly); + } + + // If a let or const fail to initialize they will remain in an unusable + // without further intervention. This call cleans up the global scope, + // setting uninitialized lexicals to undefined so that they may still + // be used. This behavior is _only_ acceptable in the context of the repl. + if (JS::ForceLexicalInitialization(cx, globalLexical) && + gErrFile->isOpen()) { + fputs( + "Warning: According to the standard, after the above exception,\n" + "Warning: the global bindings should be permanently uninitialized.\n" + "Warning: We have non-standard-ly initialized them to `undefined`" + "for you.\nWarning: This nicety only happens in the JS shell.\n", + stderr); + } + + RunShellJobs(cx); + } while (!hitEOF && !sc->quitting); + + if (gOutFile->isOpen()) { + fprintf(gOutFile->fp, "\n"); + } + + return true; +} + +enum FileKind { + PreludeScript, // UTF-8 script, fully-parsed, to avoid conflicting + // configurations. + FileScript, // UTF-8, directly parsed as such + FileScriptUtf16, // FileScript, but inflate to UTF-16 before parsing + FileModule, +}; + +[[nodiscard]] static bool Process(JSContext* cx, const char* filename, + bool forceTTY, FileKind kind) { + FILE* file; + if (forceTTY || !filename || strcmp(filename, "-") == 0) { + file = stdin; + } else { + file = OpenFile(cx, filename, "rb"); + if (!file) { + return false; + } + } + AutoCloseFile autoClose(file); + + bool fullParse = false; + if (!forceTTY && !isatty(fileno(file))) { + // It's not interactive - just execute it. + switch (kind) { + case PreludeScript: + fullParse = true; + if (!RunFile(cx, filename, file, CompileUtf8::DontInflate, compileOnly, + fullParse)) { + return false; + } + break; + case FileScript: + if (!RunFile(cx, filename, file, CompileUtf8::DontInflate, compileOnly, + fullParse)) { + return false; + } + break; + case FileScriptUtf16: + if (!RunFile(cx, filename, file, CompileUtf8::InflateToUtf16, + compileOnly, fullParse)) { + return false; + } + break; + case FileModule: + if (!RunModule(cx, filename, compileOnly)) { + return false; + } + break; + default: + MOZ_CRASH("Impossible FileKind!"); + } + } else { + // It's an interactive filehandle; drop into read-eval-print loop. + MOZ_ASSERT(kind == FileScript); + if (!ReadEvalPrintLoop(cx, file, compileOnly)) { + return false; + } + } +#ifdef FUZZING_JS_FUZZILLI + fprintf(stderr, "executionHash is 0x%x with %d inputs\n", cx->executionHash, + cx->executionHashInputs); +#endif + return true; +} + +#ifdef XP_WIN +# define GET_FD_FROM_FILE(a) int(_get_osfhandle(fileno(a))) +#else +# define GET_FD_FROM_FILE(a) fileno(a) +#endif + +static void freeExternalCallback(void* contents, void* userData) { + MOZ_ASSERT(!userData); + js_free(contents); +} + +static bool CreateExternalArrayBuffer(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorNumberASCII( + cx, my_GetErrorMessage, nullptr, + args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS, + "createExternalArrayBuffer"); + return false; + } + + int32_t bytes = 0; + if (!ToInt32(cx, args[0], &bytes)) { + return false; + } + + if (bytes < 0) { + JS_ReportErrorASCII(cx, "Size must be non-negative"); + return false; + } + + void* buffer = js_malloc(bytes); + if (!buffer) { + JS_ReportOutOfMemory(cx); + return false; + } + + RootedObject arrayBuffer( + cx, JS::NewExternalArrayBuffer(cx, bytes, buffer, &freeExternalCallback)); + if (!arrayBuffer) { + return false; + } + + args.rval().setObject(*arrayBuffer); + return true; +} + +static bool CreateMappedArrayBuffer(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1 || args.length() > 3) { + JS_ReportErrorNumberASCII( + cx, my_GetErrorMessage, nullptr, + args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS, + "createMappedArrayBuffer"); + return false; + } + + RootedString rawFilenameStr(cx, JS::ToString(cx, args[0])); + if (!rawFilenameStr) { + return false; + } + // It's a little bizarre to resolve relative to the script, but for testing + // I need a file at a known location, and the only good way I know of to do + // that right now is to include it in the repo alongside the test script. + // Bug 944164 would introduce an alternative. + Rooted filenameStr( + cx, ResolvePath(cx, rawFilenameStr, ScriptRelative)); + if (!filenameStr) { + return false; + } + UniqueChars filename = JS_EncodeStringToUTF8(cx, filenameStr); + if (!filename) { + return false; + } + + uint32_t offset = 0; + if (args.length() >= 2) { + if (!JS::ToUint32(cx, args[1], &offset)) { + return false; + } + } + + bool sizeGiven = false; + uint32_t size; + if (args.length() >= 3) { + if (!JS::ToUint32(cx, args[2], &size)) { + return false; + } + sizeGiven = true; + if (size == 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return false; + } + } + + FILE* file = OpenFile(cx, filename.get(), "rb"); + if (!file) { + return false; + } + AutoCloseFile autoClose(file); + + struct stat st; + if (fstat(fileno(file), &st) < 0) { + JS_ReportErrorASCII(cx, "Unable to stat file"); + return false; + } + + if ((st.st_mode & S_IFMT) != S_IFREG) { + JS_ReportErrorASCII(cx, "Path is not a regular file"); + return false; + } + + if (!sizeGiven) { + if (off_t(offset) >= st.st_size) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OFFSET_LARGER_THAN_FILESIZE); + return false; + } + size = st.st_size - offset; + } + + void* contents = + JS::CreateMappedArrayBufferContents(GET_FD_FROM_FILE(file), offset, size); + if (!contents) { + JS_ReportErrorASCII(cx, + "failed to allocate mapped array buffer contents " + "(possibly due to bad alignment)"); + return false; + } + + RootedObject obj(cx, + JS::NewMappedArrayBufferWithContents(cx, size, contents)); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +#undef GET_FD_FROM_FILE + +static bool AddPromiseReactions(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 3) { + JS_ReportErrorNumberASCII( + cx, my_GetErrorMessage, nullptr, + args.length() < 3 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS, + "addPromiseReactions"); + return false; + } + + RootedObject promise(cx); + if (args[0].isObject()) { + promise = &args[0].toObject(); + } + + if (!promise || !JS::IsPromiseObject(promise)) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "addPromiseReactions"); + return false; + } + + RootedObject onResolve(cx); + if (args[1].isObject()) { + onResolve = &args[1].toObject(); + } + + RootedObject onReject(cx); + if (args[2].isObject()) { + onReject = &args[2].toObject(); + } + + if (!onResolve || !onResolve->is() || !onReject || + !onReject->is()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "addPromiseReactions"); + return false; + } + + return JS::AddPromiseReactions(cx, promise, onResolve, onReject); +} + +static bool IgnoreUnhandledRejections(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + ShellContext* sc = GetShellContext(cx); + sc->trackUnhandledRejections = false; + + args.rval().setUndefined(); + return true; +} + +static bool Options(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JS::ContextOptions oldContextOptions = JS::ContextOptionsRef(cx); + for (unsigned i = 0; i < args.length(); i++) { + RootedString str(cx, JS::ToString(cx, args[i])); + if (!str) { + return false; + } + + Rooted opt(cx, str->ensureLinear(cx)); + if (!opt) { + return false; + } + + if (StringEqualsLiteral(opt, "throw_on_asmjs_validation_failure")) { + JS::ContextOptionsRef(cx).toggleThrowOnAsmJSValidationFailure(); + } else if (StringEqualsLiteral(opt, "strict_mode")) { + JS::ContextOptionsRef(cx).toggleStrictMode(); + } else { + UniqueChars optChars = QuoteString(cx, opt, '"'); + if (!optChars) { + return false; + } + + JS_ReportErrorASCII(cx, + "unknown option name %s." + " The valid names are " + "throw_on_asmjs_validation_failure and strict_mode.", + optChars.get()); + return false; + } + } + + UniqueChars names = DuplicateString(""); + bool found = false; + if (names && oldContextOptions.throwOnAsmJSValidationFailure()) { + names = JS_sprintf_append(std::move(names), "%s%s", found ? "," : "", + "throw_on_asmjs_validation_failure"); + found = true; + } + if (names && oldContextOptions.strictMode()) { + names = JS_sprintf_append(std::move(names), "%s%s", found ? "," : "", + "strict_mode"); + found = true; + } + if (!names) { + JS_ReportOutOfMemory(cx); + return false; + } + + JSString* str = JS_NewStringCopyZ(cx, names.get()); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +static bool LoadScript(JSContext* cx, unsigned argc, Value* vp, + bool scriptRelative) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str(cx); + for (unsigned i = 0; i < args.length(); i++) { + str = JS::ToString(cx, args[i]); + if (!str) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "load"); + return false; + } + + str = ResolvePath(cx, str, scriptRelative ? ScriptRelative : RootRelative); + if (!str) { + JS_ReportErrorASCII(cx, "unable to resolve path"); + return false; + } + + UniqueChars filename = JS_EncodeStringToUTF8(cx, str); + if (!filename) { + return false; + } + + errno = 0; + + CompileOptions opts(cx); + opts.setIntroductionType("js shell load") + .setIsRunOnce(true) + .setNoScriptRval(true) + .setEagerDelazificationStrategy(defaultDelazificationMode); + + RootedValue unused(cx); + if (!(compileOnly + ? JS::CompileUtf8Path(cx, opts, filename.get()) != nullptr + : JS::EvaluateUtf8Path(cx, opts, filename.get(), &unused))) { + return false; + } + } + + args.rval().setUndefined(); + return true; +} + +static bool Load(JSContext* cx, unsigned argc, Value* vp) { + return LoadScript(cx, argc, vp, false); +} + +static bool LoadScriptRelativeToScript(JSContext* cx, unsigned argc, + Value* vp) { + return LoadScript(cx, argc, vp, true); +} + +static void my_LargeAllocFailCallback() { + JSContext* cx = TlsContext.get(); + if (!cx || cx->isHelperThreadContext()) { + return; + } + + MOZ_ASSERT(!JS::RuntimeHeapIsBusy()); + + JS::PrepareForFullGC(cx); + cx->runtime()->gc.gc(JS::GCOptions::Shrink, + JS::GCReason::SHARED_MEMORY_LIMIT); +} + +static const uint32_t CacheEntry_SOURCE = 0; +static const uint32_t CacheEntry_BYTECODE = 1; +static const uint32_t CacheEntry_OPTIONS = 2; + +// Some compile options can't be combined differently between save and load. +// +// CacheEntries store a CacheOption set, and on load an exception is thrown +// if the entries are incompatible. + +enum CacheOptions : uint32_t { + IsRunOnce, + NoScriptRval, + Global, + NonSyntactic, + SourceIsLazy, + ForceFullParse, +}; + +struct CacheOptionSet : public mozilla::EnumSet { + using mozilla::EnumSet::EnumSet; + + explicit CacheOptionSet(const CompileOptions& options) : EnumSet() { + initFromOptions(options); + } + + void initFromOptions(const CompileOptions& options) { + if (options.noScriptRval) { + *this += CacheOptions::NoScriptRval; + } + if (options.isRunOnce) { + *this += CacheOptions::IsRunOnce; + } + if (options.sourceIsLazy) { + *this += CacheOptions::SourceIsLazy; + } + if (options.forceFullParse()) { + *this += CacheOptions::ForceFullParse; + } + if (options.nonSyntacticScope) { + *this += CacheOptions::NonSyntactic; + } + } +}; + +static bool CacheOptionsCompatible(const CacheOptionSet& a, + const CacheOptionSet& b) { + // If the options are identical, they are trivially compatible. + return a == b; +} + +static const JSClass CacheEntry_class = {"CacheEntryObject", + JSCLASS_HAS_RESERVED_SLOTS(3)}; + +static bool CacheEntry(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isString()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "CacheEntry"); + return false; + } + + RootedObject obj(cx, JS_NewObject(cx, &CacheEntry_class)); + if (!obj) { + return false; + } + + JS::SetReservedSlot(obj, CacheEntry_SOURCE, args[0]); + JS::SetReservedSlot(obj, CacheEntry_BYTECODE, UndefinedValue()); + + // Fill in empty option set. + CacheOptionSet defaultOptions; + JS::SetReservedSlot(obj, CacheEntry_OPTIONS, + Int32Value(defaultOptions.serialize())); + + args.rval().setObject(*obj); + return true; +} + +static bool CacheEntry_isCacheEntry(JSObject* cache) { + return cache->hasClass(&CacheEntry_class); +} + +static JSString* CacheEntry_getSource(JSContext* cx, HandleObject cache) { + MOZ_ASSERT(CacheEntry_isCacheEntry(cache)); + Value v = JS::GetReservedSlot(cache, CacheEntry_SOURCE); + if (!v.isString()) { + JS_ReportErrorASCII( + cx, "CacheEntry_getSource: Unexpected type of source reserved slot."); + return nullptr; + } + + return v.toString(); +} + +static bool CacheEntry_compatible(JSContext* cx, HandleObject cache, + const CacheOptionSet& currentOptionSet) { + CacheOptionSet cacheEntryOptions; + MOZ_ASSERT(CacheEntry_isCacheEntry(cache)); + Value v = JS::GetReservedSlot(cache, CacheEntry_OPTIONS); + cacheEntryOptions.deserialize(v.toInt32()); + if (!CacheOptionsCompatible(cacheEntryOptions, currentOptionSet)) { + JS_ReportErrorASCII(cx, + "CacheEntry_compatible: Incompatible cache contents"); + return false; + } + return true; +} + +static uint8_t* CacheEntry_getBytecode(JSContext* cx, HandleObject cache, + size_t* length) { + MOZ_ASSERT(CacheEntry_isCacheEntry(cache)); + Value v = JS::GetReservedSlot(cache, CacheEntry_BYTECODE); + if (!v.isObject() || !v.toObject().is()) { + JS_ReportErrorASCII( + cx, + "CacheEntry_getBytecode: Unexpected type of bytecode reserved slot."); + return nullptr; + } + + ArrayBufferObject* arrayBuffer = &v.toObject().as(); + *length = arrayBuffer->byteLength(); + return arrayBuffer->dataPointer(); +} + +static bool CacheEntry_setBytecode(JSContext* cx, HandleObject cache, + const CacheOptionSet& cacheOptions, + uint8_t* buffer, uint32_t length) { + MOZ_ASSERT(CacheEntry_isCacheEntry(cache)); + + using BufferContents = ArrayBufferObject::BufferContents; + + BufferContents contents = BufferContents::createMalloced(buffer); + Rooted arrayBuffer( + cx, ArrayBufferObject::createForContents(cx, length, contents)); + if (!arrayBuffer) { + return false; + } + + JS::SetReservedSlot(cache, CacheEntry_BYTECODE, ObjectValue(*arrayBuffer)); + JS::SetReservedSlot(cache, CacheEntry_OPTIONS, + Int32Value(cacheOptions.serialize())); + return true; +} + +static bool ConvertTranscodeResultToJSException(JSContext* cx, + JS::TranscodeResult rv) { + switch (rv) { + case JS::TranscodeResult::Ok: + return true; + + default: + [[fallthrough]]; + case JS::TranscodeResult::Failure: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII(cx, "generic warning"); + return false; + case JS::TranscodeResult::Failure_BadBuildId: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII(cx, "the build-id does not match"); + return false; + case JS::TranscodeResult::Failure_AsmJSNotSupported: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII(cx, "Asm.js is not supported by XDR"); + return false; + case JS::TranscodeResult::Failure_BadDecode: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII(cx, "XDR data corruption"); + return false; + + case JS::TranscodeResult::Throw: + MOZ_ASSERT(cx->isExceptionPending()); + return false; + } +} + +static bool Evaluate(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1 || args.length() > 2) { + JS_ReportErrorNumberASCII( + cx, my_GetErrorMessage, nullptr, + args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS, + "evaluate"); + return false; + } + + RootedString code(cx, nullptr); + RootedObject cacheEntry(cx, nullptr); + if (args[0].isString()) { + code = args[0].toString(); + } else if (args[0].isObject() && + CacheEntry_isCacheEntry(&args[0].toObject())) { + cacheEntry = &args[0].toObject(); + code = CacheEntry_getSource(cx, cacheEntry); + if (!code) { + return false; + } + } + + if (!code || (args.length() == 2 && args[1].isPrimitive())) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "evaluate"); + return false; + } + + RootedObject opts(cx); + if (args.length() == 2) { + if (!args[1].isObject()) { + JS_ReportErrorASCII(cx, "evaluate: The 2nd argument must be an object"); + return false; + } + + opts = &args[1].toObject(); + } + + RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + MOZ_ASSERT(global); + + // Check "global" property before everything to use the given global's + // option as the default value. + Maybe maybeOptions; + if (opts) { + RootedValue v(cx); + if (!JS_GetProperty(cx, opts, "global", &v)) { + return false; + } + if (!v.isUndefined()) { + if (v.isObject()) { + global = js::CheckedUnwrapDynamic(&v.toObject(), cx, + /* stopAtWindowProxy = */ false); + if (!global) { + return false; + } + } + if (!global || !(JS::GetClass(global)->flags & JSCLASS_IS_GLOBAL)) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "\"global\" passed to evaluate()", "not a global object"); + return false; + } + + JSAutoRealm ar(cx, global); + maybeOptions.emplace(cx); + } + } + if (!maybeOptions) { + // If "global" property is not given, use the current global's option as + // the default value. + maybeOptions.emplace(cx); + } + + CompileOptions& options = maybeOptions.ref(); + UniqueChars fileNameBytes; + RootedString displayURL(cx); + RootedString sourceMapURL(cx); + bool catchTermination = false; + bool loadBytecode = false; + bool saveIncrementalBytecode = false; + bool execute = true; + bool assertEqBytecode = false; + JS::RootedObjectVector envChain(cx); + RootedObject callerGlobal(cx, cx->global()); + + options.setIntroductionType("js shell evaluate") + .setFileAndLine("@evaluate", 1) + .setDeferDebugMetadata(); + + RootedValue privateValue(cx); + RootedString elementAttributeName(cx); + + if (opts) { + if (!js::ParseCompileOptions(cx, options, opts, &fileNameBytes)) { + return false; + } + if (!ParseDebugMetadata(cx, opts, &privateValue, &elementAttributeName)) { + return false; + } + if (!ParseSourceOptions(cx, opts, &displayURL, &sourceMapURL)) { + return false; + } + + RootedValue v(cx); + if (!JS_GetProperty(cx, opts, "catchTermination", &v)) { + return false; + } + if (!v.isUndefined()) { + catchTermination = ToBoolean(v); + } + + if (!JS_GetProperty(cx, opts, "loadBytecode", &v)) { + return false; + } + if (!v.isUndefined()) { + loadBytecode = ToBoolean(v); + } + + if (!JS_GetProperty(cx, opts, "saveIncrementalBytecode", &v)) { + return false; + } + if (!v.isUndefined()) { + saveIncrementalBytecode = ToBoolean(v); + } + + if (!JS_GetProperty(cx, opts, "execute", &v)) { + return false; + } + if (!v.isUndefined()) { + execute = ToBoolean(v); + } + + if (!JS_GetProperty(cx, opts, "assertEqBytecode", &v)) { + return false; + } + if (!v.isUndefined()) { + assertEqBytecode = ToBoolean(v); + } + + if (!JS_GetProperty(cx, opts, "envChainObject", &v)) { + return false; + } + if (!v.isUndefined()) { + if (!v.isObject()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "\"envChainObject\" passed to evaluate()", "not an object"); + return false; + } + + JSObject* obj = &v.toObject(); + if (obj->isUnqualifiedVarObj()) { + JS_ReportErrorASCII( + cx, + "\"envChainObject\" passed to evaluate() should not be an " + "unqualified variables object"); + return false; + } + + if (!envChain.append(obj)) { + return false; + } + } + + // We cannot load or save the bytecode if we have no object where the + // bytecode cache is stored. + if (loadBytecode || saveIncrementalBytecode) { + if (!cacheEntry) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "evaluate"); + return false; + } + } + } + + if (envChain.length() != 0) { + // Wrap the envChainObject list into target realm. + JSAutoRealm ar(cx, global); + for (size_t i = 0; i < envChain.length(); ++i) { + if (!JS_WrapObject(cx, envChain[i])) { + return false; + } + } + + options.setNonSyntacticScope(true); + } + + // The `loadBuffer` we use below outlives the Stencil we generate so we can + // use its contents directly in the Stencil. + options.borrowBuffer = true; + + // We need to track the options used to generate bytecode for a CacheEntry to + // avoid mismatches. This is primarily a concern when fuzzing the jsshell. + CacheOptionSet cacheOptions; + cacheOptions.initFromOptions(options); + + JS::TranscodeBuffer loadBuffer; + JS::TranscodeBuffer saveBuffer; + + if (loadBytecode) { + size_t loadLength = 0; + uint8_t* loadData = nullptr; + + if (!CacheEntry_compatible(cx, cacheEntry, cacheOptions)) { + return false; + } + + loadData = CacheEntry_getBytecode(cx, cacheEntry, &loadLength); + if (!loadData) { + return false; + } + if (!loadBuffer.append(loadData, loadLength)) { + JS_ReportOutOfMemory(cx); + return false; + } + } + + { + JSAutoRealm ar(cx, global); + RefPtr stencil; + + if (loadBytecode) { + JS::TranscodeRange range(loadBuffer.begin(), loadBuffer.length()); + JS::DecodeOptions decodeOptions(options); + + JS::TranscodeResult rv = + JS::DecodeStencil(cx, decodeOptions, range, getter_AddRefs(stencil)); + if (!ConvertTranscodeResultToJSException(cx, rv)) { + return false; + } + } else { + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, code)) { + return false; + } + + JS::SourceText srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + stencil = JS::CompileGlobalScriptToStencil(cx, options, srcBuf); + if (!stencil) { + return false; + } + } + + JS::InstantiateOptions instantiateOptions(options); + RootedScript script( + cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil)); + if (!script) { + return false; + } + + AutoReportFrontendContext fc(cx); + if (!SetSourceOptions(cx, &fc, script->scriptSource(), displayURL, + sourceMapURL)) { + return false; + } + + if (!JS::UpdateDebugMetadata(cx, script, instantiateOptions, privateValue, + elementAttributeName, nullptr, nullptr)) { + return false; + } + + if (saveIncrementalBytecode) { + if (!JS::StartIncrementalEncoding(cx, std::move(stencil))) { + return false; + } + } + + if (execute) { + if (!(envChain.empty() + ? JS_ExecuteScript(cx, script, args.rval()) + : JS_ExecuteScript(cx, envChain, script, args.rval()))) { + if (catchTermination && !JS_IsExceptionPending(cx)) { + JSAutoRealm ar1(cx, callerGlobal); + JSString* str = JS_NewStringCopyZ(cx, "terminated"); + if (!str) { + return false; + } + args.rval().setString(str); + return true; + } + return false; + } + } + + // Serialize the encoded bytecode, recorded before the execution, into a + // buffer which can be deserialized linearly. + if (saveIncrementalBytecode) { + if (!FinishIncrementalEncoding(cx, script, saveBuffer)) { + return false; + } + } + } + + if (saveIncrementalBytecode) { + // If we are both loading and saving, we assert that we are going to + // replace the current bytecode by the same stream of bytes. + if (loadBytecode && assertEqBytecode) { + if (saveBuffer.length() != loadBuffer.length()) { + char loadLengthStr[16]; + SprintfLiteral(loadLengthStr, "%zu", loadBuffer.length()); + char saveLengthStr[16]; + SprintfLiteral(saveLengthStr, "%zu", saveBuffer.length()); + + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_CACHE_EQ_SIZE_FAILED, loadLengthStr, + saveLengthStr); + return false; + } + + if (!ArrayEqual(loadBuffer.begin(), saveBuffer.begin(), + loadBuffer.length())) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_CACHE_EQ_CONTENT_FAILED); + return false; + } + } + + size_t saveLength = saveBuffer.length(); + if (saveLength >= INT32_MAX) { + JS_ReportErrorASCII(cx, "Cannot save large cache entry content"); + return false; + } + uint8_t* saveData = saveBuffer.extractOrCopyRawBuffer(); + if (!CacheEntry_setBytecode(cx, cacheEntry, cacheOptions, saveData, + saveLength)) { + js_free(saveData); + return false; + } + } + + return JS_WrapValue(cx, args.rval()); +} + +JSString* js::shell::FileAsString(JSContext* cx, JS::HandleString pathnameStr) { + UniqueChars pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + + FILE* file = OpenFile(cx, pathname.get(), "rb"); + if (!file) { + return nullptr; + } + + AutoCloseFile autoClose(file); + + struct stat st; + if (fstat(fileno(file), &st) != 0) { + JS_ReportErrorUTF8(cx, "can't stat %s", pathname.get()); + return nullptr; + } + + if ((st.st_mode & S_IFMT) != S_IFREG) { + JS_ReportErrorUTF8(cx, "can't read non-regular file %s", pathname.get()); + return nullptr; + } + + size_t len; + if (!FileSize(cx, pathname.get(), file, &len)) { + return nullptr; + } + + UniqueChars buf(js_pod_malloc(len + 1)); + if (!buf) { + JS_ReportErrorUTF8(cx, "out of memory reading %s", pathname.get()); + return nullptr; + } + + if (!ReadFile(cx, pathname.get(), file, buf.get(), len)) { + return nullptr; + } + + UniqueTwoByteChars ucbuf( + JS::LossyUTF8CharsToNewTwoByteCharsZ(cx, JS::UTF8Chars(buf.get(), len), + &len, js::MallocArena) + .get()); + if (!ucbuf) { + JS_ReportErrorUTF8(cx, "Invalid UTF-8 in file '%s'", pathname.get()); + return nullptr; + } + + return JS_NewUCStringCopyN(cx, ucbuf.get(), len); +} + +/* + * Function to run scripts and return compilation + execution time. Semantics + * are closely modelled after the equivalent function in WebKit, as this is used + * to produce benchmark timings by SunSpider. + */ +static bool Run(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "run"); + return false; + } + + RootedString str(cx, JS::ToString(cx, args[0])); + if (!str) { + return false; + } + args[0].setString(str); + + str = FileAsString(cx, str); + if (!str) { + return false; + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, str)) { + return false; + } + + JS::SourceText srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + RootedScript script(cx); + int64_t startClock = PRMJ_Now(); + { + UniqueChars filename = JS_EncodeStringToUTF8(cx, str); + if (!filename) { + return false; + } + + JS::CompileOptions options(cx); + options.setIntroductionType("js shell run") + .setFileAndLine(filename.get(), 1) + .setIsRunOnce(true) + .setNoScriptRval(true) + .setEagerDelazificationStrategy(defaultDelazificationMode); + + script = JS::Compile(cx, options, srcBuf); + if (!script) { + return false; + } + } + + if (!JS_ExecuteScript(cx, script)) { + return false; + } + + int64_t endClock = PRMJ_Now(); + + args.rval().setDouble((endClock - startClock) / double(PRMJ_USEC_PER_MSEC)); + return true; +} + +static int js_fgets(char* buf, int size, FILE* file) { + int n, i, c; + bool crflag; + + n = size - 1; + if (n < 0) { + return -1; + } + + // Use the fastest available getc. + auto fast_getc = +#if defined(HAVE_GETC_UNLOCKED) + getc_unlocked +#elif defined(HAVE__GETC_NOLOCK) + _getc_nolock +#else + getc +#endif + ; + + crflag = false; + for (i = 0; i < n && (c = fast_getc(file)) != EOF; i++) { + buf[i] = c; + if (c == '\n') { // any \n ends a line + i++; // keep the \n; we know there is room for \0 + break; + } + if (crflag) { // \r not followed by \n ends line at the \r + ungetc(c, file); + break; // and overwrite c in buf with \0 + } + crflag = (c == '\r'); + } + + buf[i] = '\0'; + return i; +} + +/* + * function readline() + * Provides a hook for scripts to read a line from stdin. + */ +static bool ReadLine(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + static constexpr size_t BUFSIZE = 256; + FILE* from = stdin; + size_t buflength = 0; + size_t bufsize = BUFSIZE; + char* buf = (char*)JS_malloc(cx, bufsize); + if (!buf) { + return false; + } + + bool sawNewline = false; + size_t gotlength; + while ((gotlength = js_fgets(buf + buflength, bufsize - buflength, from)) > + 0) { + buflength += gotlength; + + /* Are we done? */ + if (buf[buflength - 1] == '\n') { + buf[buflength - 1] = '\0'; + sawNewline = true; + break; + } else if (buflength < bufsize - 1) { + break; + } + + /* Else, grow our buffer for another pass. */ + char* tmp; + bufsize *= 2; + if (bufsize > buflength) { + tmp = static_cast(JS_realloc(cx, buf, bufsize / 2, bufsize)); + } else { + JS_ReportOutOfMemory(cx); + tmp = nullptr; + } + + if (!tmp) { + JS_free(cx, buf); + return false; + } + + buf = tmp; + } + + /* Treat the empty string specially. */ + if (buflength == 0) { + args.rval().set(feof(from) ? NullValue() : JS_GetEmptyStringValue(cx)); + JS_free(cx, buf); + return true; + } + + /* Shrink the buffer to the real size. */ + char* tmp = static_cast(JS_realloc(cx, buf, bufsize, buflength)); + if (!tmp) { + JS_free(cx, buf); + return false; + } + + buf = tmp; + + /* + * Turn buf into a JSString. Note that buflength includes the trailing null + * character. + */ + JSString* str = + JS_NewStringCopyN(cx, buf, sawNewline ? buflength - 1 : buflength); + JS_free(cx, buf); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/* + * function readlineBuf() + * Provides a hook for scripts to emulate readline() using a string object. + */ +static bool ReadLineBuf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + ShellContext* sc = GetShellContext(cx); + + if (!args.length()) { + if (!sc->readLineBuf) { + JS_ReportErrorASCII(cx, + "No source buffer set. You must initially " + "call readlineBuf with an argument."); + return false; + } + + char* currentBuf = sc->readLineBuf.get() + sc->readLineBufPos; + size_t buflen = strlen(currentBuf); + + if (!buflen) { + args.rval().setNull(); + return true; + } + + size_t len = 0; + while (len < buflen) { + if (currentBuf[len] == '\n') { + break; + } + len++; + } + + JSString* str = JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(currentBuf, len)); + if (!str) { + return false; + } + + if (currentBuf[len] == '\0') { + sc->readLineBufPos += len; + } else { + sc->readLineBufPos += len + 1; + } + + args.rval().setString(str); + return true; + } + + if (args.length() == 1) { + sc->readLineBuf = nullptr; + sc->readLineBufPos = 0; + + RootedString str(cx, JS::ToString(cx, args[0])); + if (!str) { + return false; + } + sc->readLineBuf = JS_EncodeStringToUTF8(cx, str); + if (!sc->readLineBuf) { + return false; + } + + args.rval().setUndefined(); + return true; + } + + JS_ReportErrorASCII(cx, "Must specify at most one argument"); + return false; +} + +static bool PutStr(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 0) { + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + RootedString str(cx, JS::ToString(cx, args[0])); + if (!str) { + return false; + } + UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); + if (!bytes) { + return false; + } + fputs(bytes.get(), gOutFile->fp); + fflush(gOutFile->fp); + } + + args.rval().setUndefined(); + return true; +} + +static bool Now(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + double now = PRMJ_Now() / double(PRMJ_USEC_PER_MSEC); + args.rval().setDouble(now); + return true; +} + +static bool CpuNow(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + double now = double(std::clock()) / double(CLOCKS_PER_SEC); + args.rval().setDouble(now); + return true; +} + +static bool PrintInternal(JSContext* cx, const CallArgs& args, RCFile* file) { + if (!file->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + for (unsigned i = 0; i < args.length(); i++) { + RootedString str(cx, JS::ToString(cx, args[i])); + if (!str) { + return false; + } + UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); + if (!bytes) { + return false; + } + fprintf(file->fp, "%s%s", i ? " " : "", bytes.get()); + } + + fputc('\n', file->fp); + fflush(file->fp); + + args.rval().setUndefined(); + return true; +} + +static bool Print(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); +#ifdef FUZZING_INTERFACES + if (fuzzHaveModule && !fuzzDoDebug) { + // When fuzzing and not debugging, suppress any print() output, + // as it slows down fuzzing and makes libFuzzer's output hard + // to read. + args.rval().setUndefined(); + return true; + } +#endif // FUZZING_INTERFACES + return PrintInternal(cx, args, gOutFile); +} + +static bool PrintErr(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return PrintInternal(cx, args, gErrFile); +} + +static bool Help(JSContext* cx, unsigned argc, Value* vp); + +static bool Quit(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + + // Print a message to stderr in differential testing to help jsfunfuzz + // find uncatchable-exception bugs. + if (js::SupportDifferentialTesting()) { + fprintf(stderr, "quit called\n"); + } + + CallArgs args = CallArgsFromVp(argc, vp); + int32_t code; + if (!ToInt32(cx, args.get(0), &code)) { + return false; + } + + // The fuzzers check the shell's exit code and assume a value >= 128 means + // the process crashed (for instance, SIGSEGV will result in code 139). On + // POSIX platforms, the exit code is 8-bit and negative values can also + // result in an exit code >= 128. We restrict the value to range [0, 127] to + // avoid false positives. + if (code < 0 || code >= 128) { + JS_ReportErrorASCII(cx, "quit exit code should be in range 0-127"); + return false; + } + + js::StopDrainingJobQueue(cx); + sc->exitCode = code; + sc->quitting = true; + return false; +} + +static bool StartTimingMutator(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() > 0) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_TOO_MANY_ARGS, "startTimingMutator"); + return false; + } + + if (!cx->runtime()->gc.stats().startTimingMutator()) { + JS_ReportErrorASCII( + cx, "StartTimingMutator should only be called from outside of GC"); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool StopTimingMutator(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() > 0) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_TOO_MANY_ARGS, "stopTimingMutator"); + return false; + } + + double mutator_ms, gc_ms; + if (!cx->runtime()->gc.stats().stopTimingMutator(mutator_ms, gc_ms)) { + JS_ReportErrorASCII(cx, + "stopTimingMutator called when not timing the mutator"); + return false; + } + double total_ms = mutator_ms + gc_ms; + if (total_ms > 0 && gOutFile->isOpen()) { + fprintf(gOutFile->fp, "Mutator: %.3fms (%.1f%%), GC: %.3fms (%.1f%%)\n", + mutator_ms, mutator_ms / total_ms * 100.0, gc_ms, + gc_ms / total_ms * 100.0); + } + + args.rval().setUndefined(); + return true; +} + +static const char* ToSource(JSContext* cx, HandleValue vp, UniqueChars* bytes) { + RootedString str(cx, JS_ValueToSource(cx, vp)); + if (str) { + *bytes = JS_EncodeStringToUTF8(cx, str); + if (*bytes) { + return bytes->get(); + } + } + JS_ClearPendingException(cx); + return "<>"; +} + +static bool AssertEq(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!(args.length() == 2 || (args.length() == 3 && args[2].isString()))) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + (args.length() < 2) ? JSSMSG_NOT_ENOUGH_ARGS + : (args.length() == 3) ? JSSMSG_INVALID_ARGS + : JSSMSG_TOO_MANY_ARGS, + "assertEq"); + return false; + } + + bool same; + if (!JS::SameValue(cx, args[0], args[1], &same)) { + return false; + } + if (!same) { + UniqueChars bytes0, bytes1; + const char* actual = ToSource(cx, args[0], &bytes0); + const char* expected = ToSource(cx, args[1], &bytes1); + if (args.length() == 2) { + JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr, + JSSMSG_ASSERT_EQ_FAILED, actual, expected); + } else { + RootedString message(cx, args[2].toString()); + UniqueChars bytes2 = QuoteString(cx, message); + if (!bytes2) { + return false; + } + JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr, + JSSMSG_ASSERT_EQ_FAILED_MSG, actual, expected, + bytes2.get()); + } + return false; + } + args.rval().setUndefined(); + return true; +} + +static JSScript* GetTopScript(JSContext* cx) { + NonBuiltinScriptFrameIter iter(cx); + return iter.done() ? nullptr : iter.script(); +} + +static bool GetScriptAndPCArgs(JSContext* cx, CallArgs& args, + MutableHandleScript scriptp, int32_t* ip) { + RootedScript script(cx, GetTopScript(cx)); + *ip = 0; + if (!args.get(0).isUndefined()) { + HandleValue v = args[0]; + unsigned intarg = 0; + if (v.isObject() && JS::GetClass(&v.toObject())->isJSFunction()) { + script = TestingFunctionArgumentToScript(cx, v); + if (!script) { + return false; + } + intarg++; + } + if (!args.get(intarg).isUndefined()) { + if (!JS::ToInt32(cx, args[intarg], ip)) { + return false; + } + if ((uint32_t)*ip >= script->length()) { + JS_ReportErrorASCII(cx, "Invalid PC"); + return false; + } + } + } + + scriptp.set(script); + + return true; +} + +static bool LineToPC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_LINE2PC_USAGE); + return false; + } + + RootedScript script(cx, GetTopScript(cx)); + int32_t lineArg = 0; + if (args[0].isObject() && args[0].toObject().is()) { + script = TestingFunctionArgumentToScript(cx, args[0]); + if (!script) { + return false; + } + lineArg++; + } + + uint32_t lineno; + if (!ToUint32(cx, args.get(lineArg), &lineno)) { + return false; + } + + jsbytecode* pc = LineNumberToPC(script, lineno); + if (!pc) { + return false; + } + args.rval().setInt32(script->pcToOffset(pc)); + return true; +} + +static bool PCToLine(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedScript script(cx); + int32_t i; + unsigned lineno; + + if (!GetScriptAndPCArgs(cx, args, &script, &i)) { + return false; + } + lineno = PCToLineNumber(script, script->offsetToPC(i)); + if (!lineno) { + return false; + } + args.rval().setInt32(lineno); + return true; +} + +#if defined(DEBUG) || defined(JS_JITSPEW) + +static bool Notes(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + + for (unsigned i = 0; i < args.length(); i++) { + RootedScript script(cx, TestingFunctionArgumentToScript(cx, args[i])); + if (!script) { + return false; + } + + if (!JSScript::dumpSrcNotes(cx, script, &sprinter)) { + return false; + } + } + + JSString* str = JS_NewStringCopyZ(cx, sprinter.string()); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +namespace { + +struct DisassembleOptionParser { + unsigned argc; + Value* argv; + JSScript::DumpOptions options; + + DisassembleOptionParser(unsigned argc, Value* argv) + : argc(argc), argv(argv) {} + + bool parse(JSContext* cx) { + options.recursive = false; + + /* Read options off early arguments */ + while (argc > 0 && argv[0].isString()) { + JSString* str = argv[0].toString(); + JSLinearString* linearStr = JS_EnsureLinearString(cx, str); + if (!linearStr) { + return false; + } + if (JS_LinearStringEqualsLiteral(linearStr, "-r")) { + options.recursive = true; + } else { + break; + } + argv++; + argc--; + } + return true; + } +}; + +} /* anonymous namespace */ + +static bool DisassembleToSprinter(JSContext* cx, unsigned argc, Value* vp, + Sprinter* sprinter) { + CallArgs args = CallArgsFromVp(argc, vp); + DisassembleOptionParser p(args.length(), args.array()); + if (!p.parse(cx)) { + return false; + } + + if (p.argc == 0) { + /* Without arguments, disassemble the current script. */ + RootedScript script(cx, GetTopScript(cx)); + if (script) { + JSAutoRealm ar(cx, script); + if (!JSScript::dump(cx, script, p.options, sprinter)) { + return false; + } + } + } else { + for (unsigned i = 0; i < p.argc; i++) { + RootedFunction fun(cx); + RootedScript script(cx); + RootedValue value(cx, p.argv[i]); + if (value.isObject() && value.toObject().is()) { + script = value.toObject() + .as() + .get() + ->maybeScript(); + } else { + script = TestingFunctionArgumentToScript(cx, value, fun.address()); + } + if (!script) { + return false; + } + + if (!JSScript::dump(cx, script, p.options, sprinter)) { + return false; + } + } + } + + return !sprinter->hadOutOfMemory(); +} + +static bool DisassembleToString(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + if (!DisassembleToSprinter(cx, args.length(), vp, &sprinter)) { + return false; + } + + const char* chars = sprinter.string(); + size_t len; + JS::UniqueTwoByteChars buf( + JS::LossyUTF8CharsToNewTwoByteCharsZ( + cx, JS::UTF8Chars(chars, strlen(chars)), &len, js::MallocArena) + .get()); + if (!buf) { + return false; + } + JSString* str = JS_NewUCStringCopyN(cx, buf.get(), len); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +static bool Disassemble(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + if (!DisassembleToSprinter(cx, args.length(), vp, &sprinter)) { + return false; + } + + fprintf(gOutFile->fp, "%s\n", sprinter.string()); + args.rval().setUndefined(); + return true; +} + +static bool DisassFile(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + /* Support extra options at the start, just like Disassemble. */ + DisassembleOptionParser p(args.length(), args.array()); + if (!p.parse(cx)) { + return false; + } + + if (!p.argc) { + args.rval().setUndefined(); + return true; + } + + // We should change DisassembleOptionParser to store CallArgs. + Rooted str( + cx, JS::ToString(cx, HandleValue::fromMarkedLocation(&p.argv[0]))); + if (!str) { + return false; + } + UniqueChars filename = JS_EncodeStringToUTF8(cx, str); + if (!filename) { + return false; + } + RootedScript script(cx); + + { + CompileOptions options(cx); + options.setIntroductionType("js shell disFile") + .setFileAndLine(filename.get(), 1) + .setIsRunOnce(true) + .setNoScriptRval(true) + .setEagerDelazificationStrategy(defaultDelazificationMode); + + script = JS::CompileUtf8Path(cx, options, filename.get()); + if (!script) { + return false; + } + } + + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + if (JSScript::dump(cx, script, p.options, &sprinter)) { + return false; + } + + fprintf(gOutFile->fp, "%s\n", sprinter.string()); + + args.rval().setUndefined(); + return true; +} + +static bool DisassWithSrc(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + const size_t lineBufLen = 512; + unsigned len, line1, line2, bupline; + char linebuf[lineBufLen]; + static const char sep[] = ";-------------------------"; + + RootedScript script(cx); + for (unsigned i = 0; i < args.length(); i++) { + script = TestingFunctionArgumentToScript(cx, args[i]); + if (!script) { + return false; + } + + if (!script->filename()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_FILE_SCRIPTS_ONLY); + return false; + } + + FILE* file = OpenFile(cx, script->filename(), "rb"); + if (!file) { + return false; + } + auto closeFile = MakeScopeExit([file] { fclose(file); }); + + jsbytecode* pc = script->code(); + jsbytecode* end = script->codeEnd(); + + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + + /* burn the leading lines */ + line2 = PCToLineNumber(script, pc); + for (line1 = 0; line1 < line2 - 1; line1++) { + char* tmp = fgets(linebuf, lineBufLen, file); + if (!tmp) { + JS_ReportErrorUTF8(cx, "failed to read %s fully", script->filename()); + return false; + } + } + + bupline = 0; + while (pc < end) { + line2 = PCToLineNumber(script, pc); + + if (line2 < line1) { + if (bupline != line2) { + bupline = line2; + if (!sprinter.jsprintf("%s %3u: BACKUP\n", sep, line2)) { + return false; + } + } + } else { + if (bupline && line1 == line2) { + if (!sprinter.jsprintf("%s %3u: RESTORE\n", sep, line2)) { + return false; + } + } + bupline = 0; + while (line1 < line2) { + if (!fgets(linebuf, lineBufLen, file)) { + JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr, + JSSMSG_UNEXPECTED_EOF, script->filename()); + return false; + } + line1++; + if (!sprinter.jsprintf("%s %3u: %s", sep, line1, linebuf)) { + return false; + } + } + } + + len = + Disassemble1(cx, script, pc, script->pcToOffset(pc), true, &sprinter); + if (!len) { + return false; + } + + pc += len; + } + + fprintf(gOutFile->fp, "%s\n", sprinter.string()); + } + + args.rval().setUndefined(); + return true; +} + +#endif /* defined(DEBUG) || defined(JS_JITSPEW) */ + +#ifdef JS_CACHEIR_SPEW +static bool CacheIRHealthReport(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + js::jit::CacheIRHealth cih; + RootedScript script(cx); + + // In the case that we are calling this function from the shell and + // the environment variable is not set, AutoSpewChannel automatically + // sets and unsets the proper channel for the duration of spewing + // a health report. + AutoSpewChannel channel(cx, SpewChannel::CacheIRHealthReport, script); + if (!argc) { + // Calling CacheIRHealthReport without any arguments will create health + // reports for all scripts in the zone. + for (auto base = cx->zone()->cellIter(); !base.done(); + base.next()) { + if (!base->hasJitScript() || base->selfHosted()) { + continue; + } + + script = base->asJSScript(); + cih.healthReportForScript(cx, script, js::jit::SpewContext::Shell); + } + } else { + RootedValue value(cx, args.get(0)); + + if (value.isObject() && value.toObject().is()) { + script = + value.toObject().as().get()->maybeScript(); + } else { + script = TestingFunctionArgumentToScript(cx, args.get(0)); + } + + if (!script) { + return false; + } + + cih.healthReportForScript(cx, script, js::jit::SpewContext::Shell); + } + + args.rval().setUndefined(); + return true; +} +#endif /* JS_CACHEIR_SPEW */ + +/* Pretend we can always preserve wrappers for dummy DOM objects. */ +static bool DummyPreserveWrapperCallback(JSContext* cx, HandleObject obj) { + return true; +} + +static bool DummyHasReleasedWrapperCallback(HandleObject obj) { return true; } + +#ifdef FUZZING_JS_FUZZILLI +static bool fuzzilli_hash(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + + if (argc != 1) { + return true; + } + uint32_t hash; + JS::Handle v = args.get(0); + if (v.isInt32()) { + int32_t i = v.toInt32(); + hash = FuzzilliHashDouble((double)i); + } else if (v.isDouble()) { + double d = v.toDouble(); + d = JS::CanonicalizeNaN(d); + hash = FuzzilliHashDouble(d); + } else if (v.isNull()) { + hash = FuzzilliHashDouble(1.0); + } else if (v.isUndefined()) { + hash = FuzzilliHashDouble(2.0); + } else if (v.isBoolean()) { + hash = FuzzilliHashDouble(3.0 + v.toBoolean()); + } else if (v.isBigInt()) { + JS::BigInt* bigInt = v.toBigInt(); + hash = FuzzilliHashBigInt(bigInt); + } else if (v.isObject()) { + JSObject& obj = v.toObject(); + FuzzilliHashObject(cx, &obj); + return true; + } else { + hash = 0; + } + + cx->executionHashInputs += 1; + cx->executionHash = mozilla::RotateLeft(cx->executionHash + hash, 1); + return true; +} + +// We have to assume that the fuzzer will be able to call this function e.g. by +// enumerating the properties of the global object and eval'ing them. As such +// this function is implemented in a way that requires passing some magic value +// as first argument (with the idea being that the fuzzer won't be able to +// generate this value) which then also acts as a selector for the operation +// to perform. +static bool Fuzzilli(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString arg(cx, JS::ToString(cx, args.get(0))); + if (!arg) { + return false; + } + Rooted operation(cx, StringToLinearString(cx, arg)); + if (!operation) { + return false; + } + + if (StringEqualsAscii(operation, "FUZZILLI_CRASH")) { + int type; + if (!ToInt32(cx, args.get(1), &type)) { + return false; + } + + // With this, we can test the various ways the JS shell can crash and make + // sure that Fuzzilli is able to detect all of these failures properly. + switch (type) { + case 0: + *((int*)0x41414141) = 0x1337; + break; + case 1: + MOZ_RELEASE_ASSERT(false); + break; + case 2: + MOZ_ASSERT(false); + break; + case 3: + __asm__("int3"); + break; + default: + exit(1); + } + } else if (StringEqualsAscii(operation, "FUZZILLI_PRINT")) { + static FILE* fzliout = fdopen(REPRL_DWFD, "w"); + if (!fzliout) { + fprintf( + stderr, + "Fuzzer output channel not available, printing to stdout instead\n"); + fzliout = stdout; + } + + RootedString str(cx, JS::ToString(cx, args.get(1))); + if (!str) { + return false; + } + UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); + if (!bytes) { + return false; + } + fprintf(fzliout, "%s\n", bytes.get()); + fflush(fzliout); + } else if (StringEqualsAscii(operation, "FUZZILLI_RANDOM")) { + // This is an entropy source which can be called during fuzzing. + // Its currently used to tests whether Fuzzilli detects non-deterministic + // behavior. + args.rval().setInt32(static_cast(mozilla::RandomUint64OrDie())); + return true; + } + + args.rval().setUndefined(); + return true; +} + +static bool FuzzilliReprlGetAndRun(JSContext* cx) { + size_t scriptSize = 0; + + unsigned action; + MOZ_RELEASE_ASSERT(read(REPRL_CRFD, &action, 4) == 4); + if (action == 'cexe') { + MOZ_RELEASE_ASSERT(read(REPRL_CRFD, &scriptSize, 8) == 8); + } else { + fprintf(stderr, "Unknown action: %u\n", action); + _exit(-1); + } + + CompileOptions options(cx); + options.setIntroductionType("reprl") + .setFileAndLine("reprl", 1) + .setIsRunOnce(true) + .setNoScriptRval(true) + .setEagerDelazificationStrategy(defaultDelazificationMode); + + char* scriptSrc = static_cast(js_malloc(scriptSize)); + + char* ptr = scriptSrc; + size_t remaining = scriptSize; + while (remaining > 0) { + ssize_t rv = read(REPRL_DRFD, ptr, remaining); + if (rv <= 0) { + fprintf(stderr, "Failed to load script\n"); + _exit(-1); + } + remaining -= rv; + ptr += rv; + } + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, scriptSrc, scriptSize, + JS::SourceOwnership::TakeOwnership)) { + return false; + } + + RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + if (!script) { + return false; + } + + if (!JS_ExecuteScript(cx, script)) { + return false; + } + + return true; +} + +#endif /* FUZZING_JS_FUZZILLI */ + +static bool FuzzilliUseReprlMode(OptionParser* op) { +#ifdef FUZZING_JS_FUZZILLI + // Check if we should use REPRL mode + bool reprl_mode = op->getBoolOption("reprl"); + if (reprl_mode) { + // Check in with parent + char helo[] = "HELO"; + if (write(REPRL_CWFD, helo, 4) != 4 || read(REPRL_CRFD, helo, 4) != 4) { + reprl_mode = false; + } + + if (memcmp(helo, "HELO", 4) != 0) { + fprintf(stderr, "Invalid response from parent\n"); + _exit(-1); + } + } + return reprl_mode; +#else + return false; +#endif /* FUZZING_JS_FUZZILLI */ +} + +static bool Crash(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + MOZ_CRASH("forced crash"); + } + RootedString message(cx, JS::ToString(cx, args[0])); + if (!message) { + return false; + } + UniqueChars utf8chars = JS_EncodeStringToUTF8(cx, message); + if (!utf8chars) { + return false; + } + if (args.get(1).isObject()) { + RootedValue v(cx); + RootedObject opts(cx, &args[1].toObject()); + if (!JS_GetProperty(cx, opts, "suppress_minidump", &v)) { + return false; + } + if (v.isBoolean() && v.toBoolean()) { + js::NoteIntentionalCrash(); + } + } +#ifndef DEBUG + MOZ_ReportCrash(utf8chars.get(), __FILE__, __LINE__); +#endif + MOZ_CRASH_UNSAFE(utf8chars.get()); +} + +static bool GetSLX(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedScript script(cx); + + script = TestingFunctionArgumentToScript(cx, args.get(0)); + if (!script) { + return false; + } + args.rval().setInt32(GetScriptLineExtent(script)); + return true; +} + +static bool ThrowError(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorASCII(cx, "This is an error"); + return false; +} + +static bool CopyErrorReportToObject(JSContext* cx, JSErrorReport* report, + HandleObject obj) { + RootedString nameStr(cx); + if (report->exnType == JSEXN_WARN) { + nameStr = JS_NewStringCopyZ(cx, "Warning"); + if (!nameStr) { + return false; + } + } else { + nameStr = GetErrorTypeName(cx, report->exnType); + // GetErrorTypeName doesn't set an exception, but + // can fail for InternalError or non-error objects. + if (!nameStr) { + nameStr = cx->runtime()->emptyString; + } + } + RootedValue nameVal(cx, StringValue(nameStr)); + if (!DefineDataProperty(cx, obj, cx->names().name, nameVal)) { + return false; + } + + RootedString messageStr(cx, report->newMessageString(cx)); + if (!messageStr) { + return false; + } + RootedValue messageVal(cx, StringValue(messageStr)); + if (!DefineDataProperty(cx, obj, cx->names().message, messageVal)) { + return false; + } + + RootedValue linenoVal(cx, Int32Value(report->lineno)); + if (!DefineDataProperty(cx, obj, cx->names().lineNumber, linenoVal)) { + return false; + } + + RootedValue columnVal(cx, Int32Value(report->column)); + if (!DefineDataProperty(cx, obj, cx->names().columnNumber, columnVal)) { + return false; + } + + RootedObject notesArray(cx, CreateErrorNotesArray(cx, report)); + if (!notesArray) { + return false; + } + + RootedValue notesArrayVal(cx, ObjectValue(*notesArray)); + return DefineDataProperty(cx, obj, cx->names().notes, notesArrayVal); +} + +static bool CreateErrorReport(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // We don't have a stack here, so just initialize with null. + JS::ExceptionStack exnStack(cx, args.get(0), nullptr); + JS::ErrorReportBuilder report(cx); + if (!report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) { + return false; + } + + MOZ_ASSERT(!report.report()->isWarning()); + + RootedObject obj(cx, JS_NewPlainObject(cx)); + if (!obj) { + return false; + } + + RootedString toString(cx, NewStringCopyUTF8Z(cx, report.toStringResult())); + if (!toString) { + return false; + } + + if (!JS_DefineProperty(cx, obj, "toStringResult", toString, + JSPROP_ENUMERATE)) { + return false; + } + + if (!CopyErrorReportToObject(cx, report.report(), obj)) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +#define LAZY_STANDARD_CLASSES + +/* A class for easily testing the inner/outer object callbacks. */ +typedef struct ComplexObject { + bool isInner; + bool frozen; + JSObject* inner; + JSObject* outer; +} ComplexObject; + +static bool sandbox_enumerate(JSContext* cx, JS::HandleObject obj, + JS::MutableHandleIdVector properties, + bool enumerableOnly) { + RootedValue v(cx); + + if (!JS_GetProperty(cx, obj, "lazy", &v)) { + return false; + } + + if (!ToBoolean(v)) { + return true; + } + + return JS_NewEnumerateStandardClasses(cx, obj, properties, enumerableOnly); +} + +static bool sandbox_resolve(JSContext* cx, HandleObject obj, HandleId id, + bool* resolvedp) { + RootedValue v(cx); + if (!JS_GetProperty(cx, obj, "lazy", &v)) { + return false; + } + + if (ToBoolean(v)) { + return JS_ResolveStandardClass(cx, obj, id, resolvedp); + } + return true; +} + +static const JSClassOps sandbox_classOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + sandbox_enumerate, // newEnumerate + sandbox_resolve, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + JS_GlobalObjectTraceHook, // trace +}; + +static const JSClass sandbox_class = {"sandbox", JSCLASS_GLOBAL_FLAGS, + &sandbox_classOps}; + +static void SetStandardRealmOptions(JS::RealmOptions& options) { + options.creationOptions() + .setSharedMemoryAndAtomicsEnabled(enableSharedMemory) + .setCoopAndCoepEnabled(false) + .setWeakRefsEnabled(enableWeakRefs + ? JS::WeakRefSpecifier::EnabledWithCleanupSome + : JS::WeakRefSpecifier::Disabled) + .setToSourceEnabled(enableToSource) + .setPropertyErrorMessageFixEnabled(enablePropertyErrorMessageFix) + .setIteratorHelpersEnabled(enableIteratorHelpers) + .setShadowRealmsEnabled(enableShadowRealms) + .setArrayFromAsyncEnabled(enableArrayFromAsync) +#ifdef NIGHTLY_BUILD + .setArrayGroupingEnabled(enableArrayGrouping) + .setWellFormedUnicodeStringsEnabled(enableWellFormedUnicodeStrings) +#endif + .setChangeArrayByCopyEnabled(enableChangeArrayByCopy) +#ifdef ENABLE_NEW_SET_METHODS + .setNewSetMethodsEnabled(enableNewSetMethods) +#endif + ; +} + +[[nodiscard]] static bool CheckRealmOptions(JSContext* cx, + JS::RealmOptions& options, + JSPrincipals* principals) { + JS::RealmCreationOptions& creationOptions = options.creationOptions(); + if (creationOptions.compartmentSpecifier() != + JS::CompartmentSpecifier::ExistingCompartment) { + return true; + } + + JS::Compartment* comp = creationOptions.compartment(); + + // All realms in a compartment must be either system or non-system. + bool isSystem = + principals && principals == cx->runtime()->trustedPrincipals(); + if (isSystem != IsSystemCompartment(comp)) { + JS_ReportErrorASCII(cx, + "Cannot create system and non-system realms in the " + "same compartment"); + return false; + } + + // Debugger visibility is per-compartment, not per-realm, so make sure the + // requested visibility matches the existing compartment's. + if (creationOptions.invisibleToDebugger() != comp->invisibleToDebugger()) { + JS_ReportErrorASCII(cx, + "All the realms in a compartment must have " + "the same debugger visibility"); + return false; + } + + return true; +} + +static JSObject* NewSandbox(JSContext* cx, bool lazy) { + JS::RealmOptions options; + SetStandardRealmOptions(options); + + if (defaultToSameCompartment) { + options.creationOptions().setExistingCompartment(cx->global()); + } else { + options.creationOptions().setNewCompartmentAndZone(); + } + + JSPrincipals* principals = nullptr; + if (!CheckRealmOptions(cx, options, principals)) { + return nullptr; + } + + RootedObject obj(cx, + JS_NewGlobalObject(cx, &sandbox_class, principals, + JS::DontFireOnNewGlobalHook, options)); + if (!obj) { + return nullptr; + } + + { + JSAutoRealm ar(cx, obj); + if (!lazy && !JS::InitRealmStandardClasses(cx)) { + return nullptr; + } + + RootedValue value(cx, BooleanValue(lazy)); + if (!JS_DefineProperty(cx, obj, "lazy", value, + JSPROP_PERMANENT | JSPROP_READONLY)) { + return nullptr; + } + + JS_FireOnNewGlobalObject(cx, obj); + } + + if (!cx->compartment()->wrap(cx, &obj)) { + return nullptr; + } + return obj; +} + +static bool EvalInContext(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "evalcx", 1)) { + return false; + } + + RootedString str(cx, ToString(cx, args[0])); + if (!str) { + return false; + } + + RootedObject sobj(cx); + if (args.hasDefined(1)) { + sobj = ToObject(cx, args[1]); + if (!sobj) { + return false; + } + } + + AutoStableStringChars strChars(cx); + if (!strChars.initTwoByte(cx, str)) { + return false; + } + + mozilla::Range chars = strChars.twoByteRange(); + size_t srclen = chars.length(); + const char16_t* src = chars.begin().get(); + + bool lazy = false; + if (srclen == 4) { + if (src[0] == 'l' && src[1] == 'a' && src[2] == 'z' && src[3] == 'y') { + lazy = true; + srclen = 0; + } + } + + if (!sobj) { + sobj = NewSandbox(cx, lazy); + if (!sobj) { + return false; + } + } + + if (srclen == 0) { + args.rval().setObject(*sobj); + return true; + } + + JS::AutoFilename filename; + unsigned lineno; + + DescribeScriptedCaller(cx, &filename, &lineno); + { + sobj = UncheckedUnwrap(sobj, true); + + JSAutoRealm ar(cx, sobj); + + sobj = ToWindowIfWindowProxy(sobj); + + if (!JS_IsGlobalObject(sobj)) { + JS_ReportErrorASCII(cx, "Invalid scope argument to evalcx"); + return false; + } + + JS::CompileOptions opts(cx); + opts.setFileAndLine(filename.get(), lineno) + .setEagerDelazificationStrategy(defaultDelazificationMode); + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, src, srclen, JS::SourceOwnership::Borrowed) || + !JS::Evaluate(cx, opts, srcBuf, args.rval())) { + return false; + } + } + + if (!cx->compartment()->wrap(cx, args.rval())) { + return false; + } + + return true; +} + +static bool EnsureGeckoProfilingStackInstalled(JSContext* cx, + ShellContext* sc) { + if (cx->geckoProfiler().infraInstalled()) { + MOZ_ASSERT(sc->geckoProfilingStack); + return true; + } + + MOZ_ASSERT(!sc->geckoProfilingStack); + sc->geckoProfilingStack = MakeUnique(); + if (!sc->geckoProfilingStack) { + JS_ReportOutOfMemory(cx); + return false; + } + + SetContextProfilingStack(cx, sc->geckoProfilingStack.get()); + return true; +} + +struct WorkerInput { + JSRuntime* parentRuntime; + UniqueTwoByteChars chars; + size_t length; + + WorkerInput(JSRuntime* parentRuntime, UniqueTwoByteChars chars, size_t length) + : parentRuntime(parentRuntime), chars(std::move(chars)), length(length) {} +}; + +static void DestroyShellCompartmentPrivate(JS::GCContext* gcx, + JS::Compartment* compartment) { + auto priv = static_cast( + JS_GetCompartmentPrivate(compartment)); + js_delete(priv); +} + +static void SetWorkerContextOptions(JSContext* cx); +static bool ShellBuildId(JS::BuildIdCharVector* buildId); + +static constexpr size_t gWorkerStackSize = 2 * 128 * sizeof(size_t) * 1024; + +static void WorkerMain(UniquePtr input) { + MOZ_ASSERT(input->parentRuntime); + + JSContext* cx = JS_NewContext(8L * 1024L * 1024L, input->parentRuntime); + if (!cx) { + return; + } + + ShellContext* sc = js_new(cx); + if (!sc) { + return; + } + + auto guard = mozilla::MakeScopeExit([&] { + CancelOffThreadJobsForContext(cx); + sc->markObservers.reset(); + JS_SetContextPrivate(cx, nullptr); + js_delete(sc); + JS_DestroyContext(cx); + }); + + sc->isWorker = true; + + JS_SetContextPrivate(cx, sc); + JS_AddExtraGCRootsTracer(cx, TraceBlackRoots, nullptr); + JS_SetGrayGCRootsTracer(cx, TraceGrayRoots, nullptr); + SetWorkerContextOptions(cx); + + JS_SetFutexCanWait(cx); + JS::SetWarningReporter(cx, WarningReporter); + js::SetPreserveWrapperCallbacks(cx, DummyPreserveWrapperCallback, + DummyHasReleasedWrapperCallback); + JS_InitDestroyPrincipalsCallback(cx, ShellPrincipals::destroy); + JS_SetDestroyCompartmentCallback(cx, DestroyShellCompartmentPrivate); + + js::SetWindowProxyClass(cx, &ShellWindowProxyClass); + + js::UseInternalJobQueues(cx); + + JS::SetHostCleanupFinalizationRegistryCallback( + cx, ShellCleanupFinalizationRegistryCallback, sc); + + if (!JS::InitSelfHostedCode(cx)) { + return; + } + + EnvironmentPreparer environmentPreparer(cx); + + do { + JS::RealmOptions realmOptions; + SetStandardRealmOptions(realmOptions); + + RootedObject global(cx, NewGlobalObject(cx, realmOptions, nullptr, + ShellGlobalKind::WindowProxy, + /* immutablePrototype = */ true)); + if (!global) { + break; + } + + JSAutoRealm ar(cx, global); + + JS::ConstUTF8CharsZ path(processWideModuleLoadPath.get(), + strlen(processWideModuleLoadPath.get())); + RootedString moduleLoadPath(cx, JS_NewStringCopyUTF8Z(cx, path)); + if (!moduleLoadPath) { + return; + } + sc->moduleLoader = js::MakeUnique(); + if (!sc->moduleLoader || !sc->moduleLoader->init(cx, moduleLoadPath)) { + return; + } + + JS::CompileOptions options(cx); + options.setFileAndLine("", 1) + .setIsRunOnce(true) + .setEagerDelazificationStrategy(defaultDelazificationMode); + + AutoReportException are(cx); + JS::SourceText srcBuf; + if (!srcBuf.init(cx, input->chars.get(), input->length, + JS::SourceOwnership::Borrowed)) { + break; + } + + RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + if (!script) { + break; + } + RootedValue result(cx); + JS_ExecuteScript(cx, script, &result); + } while (0); + + KillWatchdog(cx); + JS_SetGrayGCRootsTracer(cx, nullptr, nullptr); +} + +// Workers can spawn other workers, so we need a lock to access workerThreads. +static Mutex* workerThreadsLock = nullptr; +static Vector, 0, SystemAllocPolicy> workerThreads; + +class MOZ_RAII AutoLockWorkerThreads : public LockGuard { + using Base = LockGuard; + + public: + AutoLockWorkerThreads() : Base(*workerThreadsLock) { + MOZ_ASSERT(workerThreadsLock); + } +}; + +static bool EvalInWorker(JSContext* cx, unsigned argc, Value* vp) { + if (!CanUseExtraThreads()) { + JS_ReportErrorASCII(cx, "Can't create threads with --no-threads"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isString()) { + JS_ReportErrorASCII(cx, "Invalid arguments"); + return false; + } + +#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) + if (cx->runningOOMTest) { + JS_ReportErrorASCII( + cx, "Can't create threads while running simulated OOM test"); + return false; + } +#endif + + if (!args[0].toString()->ensureLinear(cx)) { + return false; + } + + if (!workerThreadsLock) { + workerThreadsLock = js_new(mutexid::ShellWorkerThreads); + if (!workerThreadsLock) { + ReportOutOfMemory(cx); + return false; + } + } + + JSLinearString* str = &args[0].toString()->asLinear(); + + UniqueTwoByteChars chars(js_pod_malloc(str->length())); + if (!chars) { + ReportOutOfMemory(cx); + return false; + } + + CopyChars(chars.get(), *str); + + auto input = js::MakeUnique(JS_GetParentRuntime(cx), + std::move(chars), str->length()); + if (!input) { + ReportOutOfMemory(cx); + return false; + } + + UniquePtr thread; + { + AutoEnterOOMUnsafeRegion oomUnsafe; + thread = js::MakeUnique( + Thread::Options().setStackSize(gWorkerStackSize + 512 * 1024)); + if (!thread || !thread->init(WorkerMain, std::move(input))) { + oomUnsafe.crash("EvalInWorker"); + } + } + + AutoLockWorkerThreads alwt; + if (!workerThreads.append(std::move(thread))) { + ReportOutOfMemory(cx); + thread->join(); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool ShapeOf(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "shapeOf: object expected"); + return false; + } + JSObject* obj = &args[0].toObject(); + args.rval().set(JS_NumberValue(double(uintptr_t(obj->shape()) >> 3))); + return true; +} + +static bool Sleep_fn(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + TimeDuration duration = TimeDuration::FromSeconds(0.0); + if (args.length() > 0) { + double t_secs; + if (!ToNumber(cx, args[0], &t_secs)) { + return false; + } + if (std::isnan(t_secs)) { + JS_ReportErrorASCII(cx, "sleep interval is not a number"); + return false; + } + + duration = TimeDuration::FromSeconds(std::max(0.0, t_secs)); + const TimeDuration MAX_TIMEOUT_INTERVAL = + TimeDuration::FromSeconds(MAX_TIMEOUT_SECONDS); + if (duration > MAX_TIMEOUT_INTERVAL) { + JS_ReportErrorASCII(cx, "Excessive sleep interval"); + return false; + } + } + { + LockGuard guard(sc->watchdogLock); + TimeStamp toWakeup = TimeStamp::Now() + duration; + for (;;) { + sc->sleepWakeup.wait_for(guard, duration); + if (sc->serviceInterrupt) { + break; + } + auto now = TimeStamp::Now(); + if (now >= toWakeup) { + break; + } + duration = toWakeup - now; + } + } + args.rval().setUndefined(); + return !sc->serviceInterrupt; +} + +static void KillWatchdog(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + Maybe thread; + + { + LockGuard guard(sc->watchdogLock); + std::swap(sc->watchdogThread, thread); + if (thread) { + // The watchdog thread becoming Nothing is its signal to exit. + sc->watchdogWakeup.notify_one(); + } + } + if (thread) { + thread->join(); + } + + MOZ_ASSERT(!sc->watchdogThread); +} + +static void WatchdogMain(JSContext* cx) { + ThisThread::SetName("JS Watchdog"); + + ShellContext* sc = GetShellContext(cx); + + { + LockGuard guard(sc->watchdogLock); + while (sc->watchdogThread) { + auto now = TimeStamp::Now(); + if (sc->watchdogTimeout && now >= sc->watchdogTimeout.value()) { + /* + * The timeout has just expired. Request an interrupt callback + * outside the lock. + */ + sc->watchdogTimeout = Nothing(); + { + UnlockGuard unlock(guard); + CancelExecution(cx); + } + + /* Wake up any threads doing sleep. */ + sc->sleepWakeup.notify_all(); + } else { + if (sc->watchdogTimeout) { + /* + * Time hasn't expired yet. Simulate an interrupt callback + * which doesn't abort execution. + */ + JS_RequestInterruptCallback(cx); + } + + TimeDuration sleepDuration = sc->watchdogTimeout + ? TimeDuration::FromSeconds(0.1) + : TimeDuration::Forever(); + sc->watchdogWakeup.wait_for(guard, sleepDuration); + } + } + } +} + +static bool ScheduleWatchdog(JSContext* cx, double t) { + ShellContext* sc = GetShellContext(cx); + + if (t <= 0) { + LockGuard guard(sc->watchdogLock); + sc->watchdogTimeout = Nothing(); + return true; + } + +#ifdef __wasi__ + return false; +#endif + + auto interval = TimeDuration::FromSeconds(t); + auto timeout = TimeStamp::Now() + interval; + LockGuard guard(sc->watchdogLock); + if (!sc->watchdogThread) { + MOZ_ASSERT(!sc->watchdogTimeout); + sc->watchdogThread.emplace(); + AutoEnterOOMUnsafeRegion oomUnsafe; + if (!sc->watchdogThread->init(WatchdogMain, cx)) { + oomUnsafe.crash("watchdogThread.init"); + } + } else if (!sc->watchdogTimeout || timeout < sc->watchdogTimeout.value()) { + sc->watchdogWakeup.notify_one(); + } + sc->watchdogTimeout = Some(timeout); + return true; +} + +static void KillWorkerThreads(JSContext* cx) { + MOZ_ASSERT_IF(!CanUseExtraThreads(), workerThreads.empty()); + + if (!workerThreadsLock) { + MOZ_ASSERT(workerThreads.empty()); + return; + } + + while (true) { + // We need to leave the AutoLockWorkerThreads scope before we call + // js::Thread::join, to avoid deadlocks when AutoLockWorkerThreads is + // used by the worker thread. + UniquePtr thread; + { + AutoLockWorkerThreads alwt; + if (workerThreads.empty()) { + break; + } + thread = std::move(workerThreads.back()); + workerThreads.popBack(); + } + thread->join(); + } + + workerThreads.clearAndFree(); + + js_delete(workerThreadsLock); + workerThreadsLock = nullptr; +} + +static void CancelExecution(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + sc->serviceInterrupt = true; + JS_RequestInterruptCallback(cx); +} + +static bool SetTimeoutValue(JSContext* cx, double t) { + if (std::isnan(t)) { + JS_ReportErrorASCII(cx, "timeout is not a number"); + return false; + } + const TimeDuration MAX_TIMEOUT_INTERVAL = + TimeDuration::FromSeconds(MAX_TIMEOUT_SECONDS); + if (TimeDuration::FromSeconds(t) > MAX_TIMEOUT_INTERVAL) { + JS_ReportErrorASCII(cx, "Excessive timeout value"); + return false; + } + GetShellContext(cx)->timeoutInterval = t; + if (!ScheduleWatchdog(cx, t)) { + JS_ReportErrorASCII(cx, "Failed to create the watchdog"); + return false; + } + return true; +} + +static bool Timeout(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + args.rval().setNumber(sc->timeoutInterval); + return true; + } + + if (args.length() > 2) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + double t; + if (!ToNumber(cx, args[0], &t)) { + return false; + } + + if (args.length() > 1) { + RootedValue value(cx, args[1]); + if (!value.isObject() || !value.toObject().is()) { + JS_ReportErrorASCII(cx, "Second argument must be a timeout function"); + return false; + } + sc->interruptFunc = value; + sc->haveInterruptFunc = true; + } + + args.rval().setUndefined(); + return SetTimeoutValue(cx, t); +} + +static bool InterruptIf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + if (ToBoolean(args[0])) { + GetShellContext(cx)->serviceInterrupt = true; + JS_RequestInterruptCallback(cx); + } + + args.rval().setUndefined(); + return true; +} + +static bool InvokeInterruptCallbackWrapper(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + GetShellContext(cx)->serviceInterrupt = true; + JS_RequestInterruptCallback(cx); + bool interruptRv = CheckForInterrupt(cx); + + // The interrupt handler could have set a pending exception. Since we call + // back into JS, don't have it see the pending exception. If we have an + // uncatchable exception that's not propagating a debug mode forced + // return, return. + if (!interruptRv && !cx->isExceptionPending() && + !cx->isPropagatingForcedReturn()) { + return false; + } + + JS::AutoSaveExceptionState savedExc(cx); + + FixedInvokeArgs<1> iargs(cx); + + iargs[0].setBoolean(interruptRv); + + RootedValue rv(cx); + if (!js::Call(cx, args[0], UndefinedHandleValue, iargs, &rv)) { + return false; + } + + args.rval().setUndefined(); + return interruptRv; +} + +static bool SetInterruptCallback(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + RootedValue value(cx, args[0]); + if (!value.isObject() || !value.toObject().is()) { + JS_ReportErrorASCII(cx, "Argument must be a function"); + return false; + } + GetShellContext(cx)->interruptFunc = value; + GetShellContext(cx)->haveInterruptFunc = true; + + args.rval().setUndefined(); + return true; +} + +#ifdef DEBUG +// var s0 = "A".repeat(10*1024); +// interruptRegexp(/a(bc|bd)/, s0); +// first arg is regexp +// second arg is string +static bool InterruptRegexp(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + ShellContext* sc = GetShellContext(cx); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 2) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments."); + return false; + } + if (!(args[0].isObject() && args[0].toObject().is())) { + ReportUsageErrorASCII(cx, callee, + "First argument must be a regular expression."); + return false; + } + if (!args[1].isString()) { + ReportUsageErrorASCII(cx, callee, "Second argument must be a String."); + return false; + } + // Set interrupt flags + sc->serviceInterrupt = true; + js::irregexp::IsolateSetShouldSimulateInterrupt(cx->isolate); + + RootedObject regexp(cx, &args[0].toObject()); + RootedString string(cx, args[1].toString()); + int32_t lastIndex = 0; + + return js::RegExpMatcherRaw(cx, regexp, string, lastIndex, nullptr, + args.rval()); +} +#endif + +static bool SetJitCompilerOption(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 2) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments."); + return false; + } + + if (!args[0].isString()) { + ReportUsageErrorASCII(cx, callee, "First argument must be a String."); + return false; + } + + if (!args[1].isInt32()) { + ReportUsageErrorASCII(cx, callee, "Second argument must be an Int32."); + return false; + } + + // Disallow setting JIT options when there are worker threads, to avoid + // races. + if (workerThreadsLock) { + ReportUsageErrorASCII( + cx, callee, "Can't set JIT options when there are worker threads."); + return false; + } + + JSLinearString* strArg = JS_EnsureLinearString(cx, args[0].toString()); + if (!strArg) { + return false; + } + +#define JIT_COMPILER_MATCH(key, string) \ + else if (JS_LinearStringEqualsLiteral(strArg, string)) opt = \ + JSJITCOMPILER_##key; + + JSJitCompilerOption opt = JSJITCOMPILER_NOT_AN_OPTION; + if (false) { + } + JIT_COMPILER_OPTIONS(JIT_COMPILER_MATCH); +#undef JIT_COMPILER_MATCH + + if (opt == JSJITCOMPILER_NOT_AN_OPTION) { + ReportUsageErrorASCII( + cx, callee, + "First argument does not name a valid option (see jsapi.h)."); + return false; + } + + int32_t number = args[1].toInt32(); + if (number < 0) { + number = -1; + } + + // Disallow enabling or disabling the Baseline Interpreter at runtime. + // Enabling is a problem because the Baseline Interpreter code is only + // present if the interpreter was enabled when the JitRuntime was created. + // To support disabling we would have to discard all JitScripts. Furthermore, + // we really want JitOptions to be immutable after startup so it's better to + // use shell flags. + if (opt == JSJITCOMPILER_BASELINE_INTERPRETER_ENABLE && + bool(number) != jit::IsBaselineInterpreterEnabled()) { + JS_ReportErrorASCII(cx, + "Enabling or disabling the Baseline Interpreter at " + "runtime is not supported."); + return false; + } + + // Throw if disabling the JITs and there's JIT code on the stack, to avoid + // assertion failures. + if ((opt == JSJITCOMPILER_BASELINE_ENABLE || + opt == JSJITCOMPILER_ION_ENABLE) && + number == 0) { + js::jit::JitActivationIterator iter(cx); + if (!iter.done()) { + JS_ReportErrorASCII(cx, + "Can't turn off JITs with JIT code on the stack."); + return false; + } + } + + // Throw if trying to disable all the Wasm compilers. The logic here is that + // if we're trying to disable a compiler that is currently enabled and that is + // the last compiler enabled then we must throw. + // + // Note that this check does not prevent an error from being thrown later. + // Actual compiler availability is dynamic and depends on other conditions, + // such as other options set and whether a debugger is present. + if ((opt == JSJITCOMPILER_WASM_JIT_BASELINE || + opt == JSJITCOMPILER_WASM_JIT_OPTIMIZING) && + number == 0) { + uint32_t baseline, optimizing; + MOZ_ALWAYS_TRUE(JS_GetGlobalJitCompilerOption( + cx, JSJITCOMPILER_WASM_JIT_BASELINE, &baseline)); + MOZ_ALWAYS_TRUE(JS_GetGlobalJitCompilerOption( + cx, JSJITCOMPILER_WASM_JIT_OPTIMIZING, &optimizing)); + if (baseline + optimizing == 1) { + if ((opt == JSJITCOMPILER_WASM_JIT_BASELINE && baseline) || + (opt == JSJITCOMPILER_WASM_JIT_OPTIMIZING && optimizing)) { + JS_ReportErrorASCII( + cx, + "Disabling all the Wasm compilers at runtime is not supported."); + return false; + } + } + } + + // JIT compiler options are process-wide, so we have to stop off-thread + // compilations for all runtimes to avoid races. + WaitForAllHelperThreads(); + + // Only release JIT code for the current runtime because there's no good + // way to discard code for other runtimes. + ReleaseAllJITCode(cx->gcContext()); + + JS_SetGlobalJitCompilerOption(cx, opt, uint32_t(number)); + + args.rval().setUndefined(); + return true; +} + +static bool EnableLastWarning(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + sc->lastWarningEnabled = true; + sc->lastWarning.setNull(); + + args.rval().setUndefined(); + return true; +} + +static bool DisableLastWarning(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + sc->lastWarningEnabled = false; + sc->lastWarning.setNull(); + + args.rval().setUndefined(); + return true; +} + +static bool GetLastWarning(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + if (!sc->lastWarningEnabled) { + JS_ReportErrorASCII(cx, "Call enableLastWarning first."); + return false; + } + + if (!JS_WrapValue(cx, &sc->lastWarning)) { + return false; + } + + args.rval().set(sc->lastWarning); + return true; +} + +static bool ClearLastWarning(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + if (!sc->lastWarningEnabled) { + JS_ReportErrorASCII(cx, "Call enableLastWarning first."); + return false; + } + + sc->lastWarning.setNull(); + + args.rval().setUndefined(); + return true; +} + +#if defined(DEBUG) || defined(JS_JITSPEW) +static bool StackDump(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + bool showArgs = ToBoolean(args.get(0)); + bool showLocals = ToBoolean(args.get(1)); + bool showThisProps = ToBoolean(args.get(2)); + + JS::UniqueChars buf = + JS::FormatStackDump(cx, showArgs, showLocals, showThisProps); + if (!buf) { + fputs("Failed to format JavaScript stack for dump\n", gOutFile->fp); + JS_ClearPendingException(cx); + } else { + fputs(buf.get(), gOutFile->fp); + } + + args.rval().setUndefined(); + return true; +} +#endif + +static bool StackPointerInfo(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Copy the truncated stack pointer to the result. This value is not used + // as a pointer but as a way to measure frame-size from JS. + args.rval().setInt32(int32_t(reinterpret_cast(&args) & 0xfffffff)); + return true; +} + +static bool Elapsed(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + double d = PRMJ_Now() - GetShellContext(cx)->startTime; + args.rval().setDouble(d); + return true; + } + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; +} + +static ShellCompartmentPrivate* EnsureShellCompartmentPrivate(JSContext* cx) { + Compartment* comp = cx->compartment(); + auto priv = + static_cast(JS_GetCompartmentPrivate(comp)); + if (!priv) { + priv = cx->new_(); + JS_SetCompartmentPrivate(cx->compartment(), priv); + } + return priv; +} + +static bool ParseModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "parseModule", 1)) { + return false; + } + + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string to compile, got %s", typeName); + return false; + } + + JSString* scriptContents = args[0].toString(); + + UniqueChars filename; + CompileOptions options(cx); + if (args.length() > 1) { + if (!args[1].isString()) { + const char* typeName = InformalValueTypeName(args[1]); + JS_ReportErrorASCII(cx, "expected filename string, got %s", typeName); + return false; + } + + RootedString str(cx, args[1].toString()); + filename = JS_EncodeStringToUTF8(cx, str); + if (!filename) { + return false; + } + + options.setFileAndLine(filename.get(), 1); + } else { + options.setFileAndLine("", 1); + } + options.setModule(); + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, scriptContents)) { + return false; + } + + JS::SourceText srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + AutoReportFrontendContext fc(cx); + RootedObject module(cx, frontend::CompileModule(cx, &fc, options, srcBuf)); + if (!module) { + return false; + } + + Rooted wrapper( + cx, ShellModuleObjectWrapper::create(cx, module.as())); + if (!wrapper) { + return false; + } + args.rval().setObject(*wrapper); + return true; +} + +// A JSObject that holds XDRBuffer. +class XDRBufferObject : public NativeObject { + static const size_t VECTOR_SLOT = 0; + static const unsigned RESERVED_SLOTS = 1; + + public: + static const JSClassOps classOps_; + static const JSClass class_; + + [[nodiscard]] inline static XDRBufferObject* create( + JSContext* cx, JS::TranscodeBuffer&& buf); + + JS::TranscodeBuffer* data() const { + Value value = getReservedSlot(VECTOR_SLOT); + auto buf = static_cast(value.toPrivate()); + MOZ_ASSERT(buf); + return buf; + } + + bool hasData() const { + // Data may not be present if we hit OOM in initialization. + return !getReservedSlot(VECTOR_SLOT).isUndefined(); + } + + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +/*static */ const JSClassOps XDRBufferObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + XDRBufferObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +/*static */ const JSClass XDRBufferObject::class_ = { + "XDRBufferObject", + JSCLASS_HAS_RESERVED_SLOTS(XDRBufferObject::RESERVED_SLOTS) | + JSCLASS_BACKGROUND_FINALIZE, + &XDRBufferObject::classOps_}; + +XDRBufferObject* XDRBufferObject::create(JSContext* cx, + JS::TranscodeBuffer&& buf) { + XDRBufferObject* bufObj = + NewObjectWithGivenProto(cx, nullptr); + if (!bufObj) { + return nullptr; + } + + auto heapBuf = cx->make_unique(std::move(buf)); + if (!heapBuf) { + return nullptr; + } + + size_t len = heapBuf->length(); + InitReservedSlot(bufObj, VECTOR_SLOT, heapBuf.release(), len, + MemoryUse::XDRBufferElements); + + return bufObj; +} + +void XDRBufferObject::finalize(JS::GCContext* gcx, JSObject* obj) { + XDRBufferObject* buf = &obj->as(); + if (buf->hasData()) { + gcx->delete_(buf, buf->data(), buf->data()->length(), + MemoryUse::XDRBufferElements); + } +} + +static bool InstantiateModuleStencil(JSContext* cx, uint32_t argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "instantiateModuleStencil", 1)) { + return false; + } + + /* Prepare the input byte array. */ + if (!args[0].isObject() || !args[0].toObject().is()) { + JS_ReportErrorASCII(cx, + "instantiateModuleStencil: Stencil object expected"); + return false; + } + Rooted stencilObj( + cx, &args[0].toObject().as()); + + if (!stencilObj->stencil()->isModule()) { + JS_ReportErrorASCII(cx, + "instantiateModuleStencil: Module stencil expected"); + return false; + } + + CompileOptions options(cx); + UniqueChars fileNameBytes; + if (args.length() == 2) { + if (!args[1].isObject()) { + JS_ReportErrorASCII( + cx, "instantiateModuleStencil: The 2nd argument must be an object"); + return false; + } + + RootedObject opts(cx, &args[1].toObject()); + if (!js::ParseCompileOptions(cx, options, opts, &fileNameBytes)) { + return false; + } + } + + /* Prepare the CompilationStencil for decoding. */ + AutoReportFrontendContext fc(cx); + Rooted input(cx, + frontend::CompilationInput(options)); + if (!input.get().initForModule(&fc)) { + return false; + } + + /* Instantiate the stencil. */ + Rooted output(cx); + if (!frontend::CompilationStencil::instantiateStencils( + cx, input.get(), *stencilObj->stencil(), output.get())) { + return false; + } + + Rooted modObject(cx, output.get().module); + Rooted wrapper( + cx, ShellModuleObjectWrapper::create(cx, modObject)); + if (!wrapper) { + return false; + } + args.rval().setObject(*wrapper); + return true; +} + +static bool InstantiateModuleStencilXDR(JSContext* cx, uint32_t argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "instantiateModuleStencilXDR", 1)) { + return false; + } + + /* Prepare the input byte array. */ + if (!args[0].isObject() || !args[0].toObject().is()) { + JS_ReportErrorASCII( + cx, "instantiateModuleStencilXDR: stencil XDR object expected"); + return false; + } + Rooted xdrObj( + cx, &args[0].toObject().as()); + MOZ_ASSERT(xdrObj->hasBuffer()); + + CompileOptions options(cx); + UniqueChars fileNameBytes; + if (args.length() == 2) { + if (!args[1].isObject()) { + JS_ReportErrorASCII( + cx, + "instantiateModuleStencilXDR: The 2nd argument must be an object"); + return false; + } + + RootedObject opts(cx, &args[1].toObject()); + if (!js::ParseCompileOptions(cx, options, opts, &fileNameBytes)) { + return false; + } + } + + /* Prepare the CompilationStencil for decoding. */ + AutoReportFrontendContext fc(cx); + Rooted input(cx, + frontend::CompilationInput(options)); + if (!input.get().initForModule(&fc)) { + return false; + } + frontend::CompilationStencil stencil(nullptr); + + /* Deserialize the stencil from XDR. */ + JS::TranscodeRange xdrRange(xdrObj->buffer(), xdrObj->bufferLength()); + bool succeeded = false; + if (!stencil.deserializeStencils(&fc, options, xdrRange, &succeeded)) { + return false; + } + if (!succeeded) { + fc.clearAutoReport(); + JS_ReportErrorASCII(cx, "Decoding failure"); + return false; + } + + if (!stencil.isModule()) { + fc.clearAutoReport(); + JS_ReportErrorASCII(cx, + "instantiateModuleStencilXDR: Module stencil expected"); + return false; + } + + /* Instantiate the stencil. */ + Rooted output(cx); + if (!frontend::CompilationStencil::instantiateStencils( + cx, input.get(), stencil, output.get())) { + return false; + } + + Rooted modObject(cx, output.get().module); + Rooted wrapper( + cx, ShellModuleObjectWrapper::create(cx, modObject)); + if (!wrapper) { + return false; + } + args.rval().setObject(*wrapper); + return true; +} + +static bool RegisterModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "registerModule", 2)) { + return false; + } + + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string, got %s", typeName); + return false; + } + + if (!args[1].isObject() || + !args[1].toObject().is()) { + const char* typeName = InformalValueTypeName(args[1]); + JS_ReportErrorASCII(cx, "expected module, got %s", typeName); + return false; + } + + ShellContext* sc = GetShellContext(cx); + Rooted module( + cx, args[1].toObject().as().get()); + + Rooted specifier(cx, AtomizeString(cx, args[0].toString())); + if (!specifier) { + return false; + } + + RootedObject moduleRequest( + cx, ModuleRequestObject::create(cx, specifier, nullptr)); + if (!moduleRequest) { + return false; + } + + if (!sc->moduleLoader->registerTestModule(cx, moduleRequest, module)) { + return false; + } + + Rooted wrapper( + cx, ShellModuleObjectWrapper::create(cx, module)); + if (!wrapper) { + return false; + } + args.rval().setObject(*wrapper); + return true; +} + +static bool ClearModules(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + ShellContext* sc = GetShellContext(cx); + sc->moduleLoader->clearModules(cx); + args.rval().setUndefined(); + return true; +} + +static bool ModuleLink(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_ARGS, + "moduleLink"); + return false; + } + + RootedObject object(cx, UncheckedUnwrap(&args[0].toObject())); + if (!object->is()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_ARGS, + "moduleLink"); + return false; + } + + AutoRealm ar(cx, object); + + Rooted module(cx, + object->as().get()); + if (!js::ModuleLink(cx, module)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool ModuleEvaluate(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_ARGS, + "moduleEvaluate"); + return false; + } + + RootedObject object(cx, UncheckedUnwrap(&args[0].toObject())); + if (!object->is()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_ARGS, + "moduleEvaluate"); + return false; + } + + { + AutoRealm ar(cx, object); + + Rooted module(cx, + object->as().get()); + if (!js::ModuleEvaluate(cx, module, args.rval())) { + return false; + } + } + + return JS_WrapValue(cx, args.rval()); +} + +static ModuleEnvironmentObject* GetModuleInitialEnvironment( + JSContext* cx, Handle module) { + // Use the initial environment so that tests can check bindings exists + // before they have been instantiated. + Rooted env(cx, &module->initialEnvironment()); + MOZ_ASSERT(env); + return env; +} + +static bool GetModuleEnvironmentNames(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + if (!args[0].isObject() || + !args[0].toObject().is()) { + JS_ReportErrorASCII(cx, + "First argument should be a ShellModuleObjectWrapper"); + return false; + } + + Rooted module( + cx, args[0].toObject().as().get()); + if (module->hadEvaluationError()) { + JS_ReportErrorASCII(cx, "Module environment unavailable"); + return false; + } + + Rooted env(cx, + GetModuleInitialEnvironment(cx, module)); + Rooted ids(cx, IdVector(cx)); + if (!JS_Enumerate(cx, env, &ids)) { + return false; + } + + // The "*namespace*" binding is a detail of current implementation so hide + // it to give stable results in tests. + ids.eraseIfEqual(NameToId(cx->names().starNamespaceStar)); + + uint32_t length = ids.length(); + Rooted array(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!array) { + return false; + } + + array->setDenseInitializedLength(length); + for (uint32_t i = 0; i < length; i++) { + array->initDenseElement(i, StringValue(ids[i].toString())); + } + + args.rval().setObject(*array); + return true; +} + +static bool GetModuleEnvironmentValue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 2) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + if (!args[0].isObject() || + !args[0].toObject().is()) { + JS_ReportErrorASCII(cx, + "First argument should be a ShellModuleObjectWrapper"); + return false; + } + + if (!args[1].isString()) { + JS_ReportErrorASCII(cx, "Second argument should be a string"); + return false; + } + + Rooted module( + cx, args[0].toObject().as().get()); + if (module->hadEvaluationError()) { + JS_ReportErrorASCII(cx, "Module environment unavailable"); + return false; + } + + Rooted env(cx, + GetModuleInitialEnvironment(cx, module)); + RootedString name(cx, args[1].toString()); + RootedId id(cx); + if (!JS_StringToId(cx, name, &id)) { + return false; + } + + if (!GetProperty(cx, env, env, id, args.rval())) { + return false; + } + + if (args.rval().isMagic(JS_UNINITIALIZED_LEXICAL)) { + ReportRuntimeLexicalError(cx, JSMSG_UNINITIALIZED_LEXICAL, id); + return false; + } + + return true; +} + +enum class DumpType { + ParseNode, + Stencil, +}; + +template +static bool DumpAST(JSContext* cx, const JS::ReadOnlyCompileOptions& options, + const Unit* units, size_t length, + js::frontend::CompilationState& compilationState, + js::frontend::ParseGoal goal) { + using namespace js::frontend; + + AutoReportFrontendContext fc(cx); + Parser parser(&fc, options, units, length, + /* foldConstants = */ false, + compilationState, + /* syntaxParser = */ nullptr); + if (!parser.checkOptions()) { + return false; + } + + // Emplace the top-level stencil. + MOZ_ASSERT(compilationState.scriptData.length() == + CompilationStencil::TopLevelIndex); + if (!compilationState.appendScriptStencilAndData(&fc)) { + return false; + } + + js::frontend::ParseNode* pn; + if (goal == frontend::ParseGoal::Script) { + pn = parser.parse(); + } else { + ModuleBuilder builder(&fc, &parser); + + SourceExtent extent = SourceExtent::makeGlobalExtent(length); + ModuleSharedContext modulesc(&fc, options, builder, extent); + pn = parser.moduleBody(&modulesc); + } + + if (!pn) { + return false; + } + +#if defined(DEBUG) + js::Fprinter out(stderr); + DumpParseTree(&parser, pn, out); +#endif + + return true; +} + +template +[[nodiscard]] static bool DumpStencil(JSContext* cx, + const JS::ReadOnlyCompileOptions& options, + const Unit* units, size_t length, + js::frontend::ParseGoal goal) { + Rooted input(cx, + frontend::CompilationInput(options)); + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, units, length, JS::SourceOwnership::Borrowed)) { + return false; + } + + AutoReportFrontendContext fc(cx); + js::frontend::NoScopeBindingCache scopeCache; + UniquePtr stencil; + if (goal == frontend::ParseGoal::Script) { + stencil = frontend::CompileGlobalScriptToExtensibleStencil( + cx, &fc, input.get(), &scopeCache, srcBuf, ScopeKind::Global); + } else { + stencil = frontend::ParseModuleToExtensibleStencil( + cx, &fc, cx->tempLifoAlloc(), input.get(), &scopeCache, srcBuf); + } + + if (!stencil) { + return false; + } + +#if defined(DEBUG) || defined(JS_JITSPEW) + stencil->dump(); +#endif + + return true; +} + +static bool FrontendTest(JSContext* cx, unsigned argc, Value* vp, + const char* funcName, DumpType dumpType) { + using namespace js::frontend; + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, funcName, 1)) { + return false; + } + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string to parse, got %s", typeName); + return false; + } + + frontend::ParseGoal goal = frontend::ParseGoal::Script; +#ifdef JS_ENABLE_SMOOSH + bool smoosh = false; +#endif + + CompileOptions options(cx); + options.setIntroductionType("js shell parse") + .setFileAndLine("", 1) + .setIsRunOnce(true) + .setNoScriptRval(true); + + if (args.length() >= 2) { + if (!args[1].isObject()) { + JS_ReportErrorASCII(cx, "The 2nd argument must be an object"); + return false; + } + + RootedObject objOptions(cx, &args[1].toObject()); + + RootedValue optionModule(cx); + if (!JS_GetProperty(cx, objOptions, "module", &optionModule)) { + return false; + } + + if (optionModule.isBoolean()) { + if (optionModule.toBoolean()) { + goal = frontend::ParseGoal::Module; + } + } else if (!optionModule.isUndefined()) { + const char* typeName = InformalValueTypeName(optionModule); + JS_ReportErrorASCII(cx, "option `module` should be a boolean, got %s", + typeName); + return false; + } + if (!js::ParseCompileOptions(cx, options, objOptions, nullptr)) { + return false; + } + +#ifdef JS_ENABLE_SMOOSH + bool found = false; + if (!JS_HasProperty(cx, objOptions, "rustFrontend", &found)) { + return false; + } + if (found) { + JS_ReportErrorASCII(cx, "'rustFrontend' option is renamed to 'smoosh'"); + return false; + } + + RootedValue optionSmoosh(cx); + if (!JS_GetProperty(cx, objOptions, "smoosh", &optionSmoosh)) { + return false; + } + + if (optionSmoosh.isBoolean()) { + smoosh = optionSmoosh.toBoolean(); + } else if (!optionSmoosh.isUndefined()) { + const char* typeName = InformalValueTypeName(optionSmoosh); + JS_ReportErrorASCII(cx, "option `smoosh` should be a boolean, got %s", + typeName); + return false; + } +#endif // JS_ENABLE_SMOOSH + } + + JSString* scriptContents = args[0].toString(); + Rooted linearString(cx, scriptContents->ensureLinear(cx)); + if (!linearString) { + return false; + } + + bool isAscii = false; + if (linearString->hasLatin1Chars()) { + JS::AutoCheckCannotGC nogc; + isAscii = JS::StringIsASCII(mozilla::Span( + reinterpret_cast(linearString->latin1Chars(nogc)), + linearString->length())); + } + + AutoStableStringChars stableChars(cx); + if (isAscii) { + if (!stableChars.init(cx, scriptContents)) { + return false; + } + MOZ_ASSERT(stableChars.isLatin1()); + } else { + if (!stableChars.initTwoByte(cx, scriptContents)) { + return false; + } + } + + size_t length = scriptContents->length(); +#ifdef JS_ENABLE_SMOOSH + if (dumpType == DumpType::ParseNode) { + if (smoosh) { + if (isAscii) { + const Latin1Char* chars = stableChars.latin1Range().begin().get(); + + if (goal == frontend::ParseGoal::Script) { + if (!SmooshParseScript(cx, chars, length)) { + return false; + } + } else { + if (!SmooshParseModule(cx, chars, length)) { + return false; + } + } + args.rval().setUndefined(); + return true; + } + JS_ReportErrorASCII(cx, + "SmooshMonkey does not support non-ASCII chars yet"); + return false; + } + } +#endif // JS_ENABLE_SMOOSH + + if (goal == frontend::ParseGoal::Module) { + // See frontend::CompileModule. + options.setForceStrictMode(); + options.allowHTMLComments = false; + } + + if (dumpType == DumpType::Stencil) { +#ifdef JS_ENABLE_SMOOSH + if (smoosh) { + if (isAscii) { + if (goal == frontend::ParseGoal::Script) { + const Latin1Char* latin1 = stableChars.latin1Range().begin().get(); + auto utf8 = reinterpret_cast(latin1); + JS::SourceText srcBuf; + if (!srcBuf.init(cx, utf8, length, JS::SourceOwnership::Borrowed)) { + return false; + } + + AutoReportFrontendContext fc(cx); + Rooted input( + cx, frontend::CompilationInput(options)); + UniquePtr stencil; + if (!Smoosh::tryCompileGlobalScriptToExtensibleStencil( + cx, &fc, input.get(), srcBuf, stencil)) { + return false; + } + if (!stencil) { + fc.clearAutoReport(); + JS_ReportErrorASCII(cx, "SmooshMonkey failed to parse"); + return false; + } + +# ifdef DEBUG + { + frontend::BorrowingCompilationStencil borrowingStencil(*stencil); + borrowingStencil.dump(); + } +# endif + } else { + JS_ReportErrorASCII(cx, + "SmooshMonkey does not support module stencil"); + return false; + } + args.rval().setUndefined(); + return true; + } + JS_ReportErrorASCII(cx, + "SmooshMonkey does not support non-ASCII chars yet"); + return false; + } +#endif // JS_ENABLE_SMOOSH + + if (isAscii) { + const Latin1Char* latin1 = stableChars.latin1Range().begin().get(); + auto utf8 = reinterpret_cast(latin1); + if (!DumpStencil(cx, options, utf8, length, goal)) { + return false; + } + } else { + MOZ_ASSERT(stableChars.isTwoByte()); + const char16_t* chars = stableChars.twoByteRange().begin().get(); + if (!DumpStencil(cx, options, chars, length, goal)) { + return false; + } + } + + args.rval().setUndefined(); + return true; + } + + AutoReportFrontendContext fc(cx); + Rooted input(cx, + frontend::CompilationInput(options)); + if (goal == frontend::ParseGoal::Script) { + if (!input.get().initForGlobal(&fc)) { + return false; + } + } else { + if (!input.get().initForModule(&fc)) { + return false; + } + } + + LifoAllocScope allocScope(&cx->tempLifoAlloc()); + frontend::NoScopeBindingCache scopeCache; + frontend::CompilationState compilationState(&fc, allocScope, input.get()); + if (!compilationState.init(&fc, &scopeCache)) { + return false; + } + + if (isAscii) { + const Latin1Char* latin1 = stableChars.latin1Range().begin().get(); + auto utf8 = reinterpret_cast(latin1); + if (!DumpAST(cx, options, utf8, length, compilationState, + goal)) { + return false; + } + } else { + MOZ_ASSERT(stableChars.isTwoByte()); + const char16_t* chars = stableChars.twoByteRange().begin().get(); + if (!DumpAST(cx, options, chars, length, compilationState, + goal)) { + return false; + } + } + args.rval().setUndefined(); + return true; +} + +static bool DumpStencil(JSContext* cx, unsigned argc, Value* vp) { + return FrontendTest(cx, argc, vp, "dumpStencil", DumpType::Stencil); +} + +static bool Parse(JSContext* cx, unsigned argc, Value* vp) { + // Parse returns local scope information with variables ordered + // differently, depending on the underlying JIT implementation. + if (js::SupportDifferentialTesting()) { + JS_ReportErrorASCII(cx, + "Function not available in differential testing mode."); + return false; + } + + return FrontendTest(cx, argc, vp, "parse", DumpType::ParseNode); +} + +static bool SyntaxParse(JSContext* cx, unsigned argc, Value* vp) { + using namespace js::frontend; + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "syntaxParse", 1)) { + return false; + } + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string to parse, got %s", typeName); + return false; + } + + JSString* scriptContents = args[0].toString(); + + CompileOptions options(cx); + options.setIntroductionType("js shell syntaxParse") + .setFileAndLine("", 1); + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, scriptContents)) { + return false; + } + + const char16_t* chars = stableChars.twoByteRange().begin().get(); + size_t length = scriptContents->length(); + + AutoReportFrontendContext fc(cx); + Rooted input(cx, + frontend::CompilationInput(options)); + if (!input.get().initForGlobal(&fc)) { + return false; + } + + LifoAllocScope allocScope(&cx->tempLifoAlloc()); + frontend::NoScopeBindingCache scopeCache; + frontend::CompilationState compilationState(&fc, allocScope, input.get()); + if (!compilationState.init(&fc, &scopeCache)) { + return false; + } + + Parser parser( + &fc, options, chars, length, + /* foldConstants = */ false, compilationState, + /* syntaxParser = */ nullptr); + if (!parser.checkOptions()) { + return false; + } + + bool succeeded = parser.parse(); + if (fc.hadErrors()) { + return false; + } + + if (!succeeded && !parser.hadAbortedSyntaxParse()) { + // If no exception is posted, either there was an OOM or a language + // feature unhandled by the syntax parser was encountered. + MOZ_ASSERT(fc.hadOutOfMemory()); + return false; + } + + args.rval().setBoolean(succeeded); + return true; +} + +static void OffThreadCompileScriptCallback(JS::OffThreadToken* token, + void* callbackData) { + auto job = static_cast(callbackData); + job->markDone(token); +} + +static bool OffThreadCompileToStencil(JSContext* cx, unsigned argc, Value* vp) { + if (!CanUseExtraThreads()) { + JS_ReportErrorASCII( + cx, "Can't use offThreadCompileToStencil with --no-threads"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "offThreadCompileToStencil", 1)) { + return false; + } + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string to parse, got %s", typeName); + return false; + } + + UniqueChars fileNameBytes; + CompileOptions options(cx); + options.setIntroductionType("js shell offThreadCompileToStencil") + .setFileAndLine("", 1); + + if (args.length() >= 2) { + if (!args[1].isObject()) { + JS_ReportErrorASCII( + cx, "offThreadCompileToStencil: The 2nd argument must be an object"); + return false; + } + + // Offthread compilation requires that the debug metadata be set when the + // script is collected from offthread, rather than when compiled. + RootedObject opts(cx, &args[1].toObject()); + if (!js::ParseCompileOptions(cx, options, opts, &fileNameBytes)) { + return false; + } + } + + // This option setting must override whatever the caller requested. + options.setIsRunOnce(true); + + // We assume the caller wants caching if at all possible, ignoring + // heuristics that make sense for a real browser. + options.forceAsync = true; + + JSString* scriptContents = args[0].toString(); + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, scriptContents)) { + return false; + } + + size_t length = scriptContents->length(); + const char16_t* chars = stableChars.twoByteChars(); + + // Make sure we own the string's chars, so that they are not freed before + // the compilation is finished. + UniqueTwoByteChars ownedChars; + if (stableChars.maybeGiveOwnershipToCaller()) { + ownedChars.reset(const_cast(chars)); + } else { + ownedChars.reset(cx->pod_malloc(length)); + if (!ownedChars) { + return false; + } + + mozilla::PodCopy(ownedChars.get(), chars, length); + } + + if (!JS::CanCompileOffThread(cx, options, length)) { + JS_ReportErrorASCII(cx, "cannot compile code on worker thread"); + return false; + } + + OffThreadJob* job = + NewOffThreadJob(cx, options, OffThreadJob::Source(std::move(ownedChars))); + if (!job) { + return false; + } + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, job->sourceChars(), length, + JS::SourceOwnership::Borrowed) || + !JS::CompileToStencilOffThread(cx, options, srcBuf, + OffThreadCompileScriptCallback, job)) { + job->cancel(); + DeleteOffThreadJob(cx, job); + return false; + } + + args.rval().setInt32(job->id); + return true; +} + +static bool FinishOffThreadStencil(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + OffThreadJob* job = LookupOffThreadJobForArgs(cx, args, 0); + if (!job) { + return false; + } + + JS::OffThreadToken* token = job->waitUntilDone(cx); + MOZ_ASSERT(token); + + RefPtr stencil = JS::FinishOffThreadStencil(cx, token); + DeleteOffThreadJob(cx, job); + if (!stencil) { + return false; + } + RootedObject stencilObj(cx, + js::StencilObject::create(cx, std::move(stencil))); + if (!stencilObj) { + return false; + } + + args.rval().setObject(*stencilObj); + return true; +} + +static bool OffThreadCompileModuleToStencil(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isString()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, + "offThreadCompileModuleToStencil"); + return false; + } + + UniqueChars fileNameBytes; + CompileOptions options(cx); + options.setIntroductionType("js shell offThreadCompileModuleToStencil") + .setFileAndLine("", 1); + options.setIsRunOnce(true).setSourceIsLazy(false); + options.forceAsync = true; + + JSString* scriptContents = args[0].toString(); + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, scriptContents)) { + return false; + } + + size_t length = scriptContents->length(); + const char16_t* chars = stableChars.twoByteChars(); + + // Make sure we own the string's chars, so that they are not freed before + // the compilation is finished. + UniqueTwoByteChars ownedChars; + if (stableChars.maybeGiveOwnershipToCaller()) { + ownedChars.reset(const_cast(chars)); + } else { + ownedChars.reset(cx->pod_malloc(length)); + if (!ownedChars) { + return false; + } + + mozilla::PodCopy(ownedChars.get(), chars, length); + } + + if (!JS::CanCompileOffThread(cx, options, length)) { + JS_ReportErrorASCII(cx, "cannot compile code on worker thread"); + return false; + } + + OffThreadJob* job = + NewOffThreadJob(cx, options, OffThreadJob::Source(std::move(ownedChars))); + if (!job) { + return false; + } + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, job->sourceChars(), length, + JS::SourceOwnership::Borrowed) || + !JS::CompileModuleToStencilOffThread( + cx, options, srcBuf, OffThreadCompileScriptCallback, job)) { + job->cancel(); + DeleteOffThreadJob(cx, job); + return false; + } + + args.rval().setInt32(job->id); + return true; +} + +static bool OffThreadDecodeStencil(JSContext* cx, unsigned argc, Value* vp) { + if (!CanUseExtraThreads()) { + JS_ReportErrorASCII(cx, + "Can't use offThreadDecodeStencil with --no-threads"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "offThreadDecodeStencil", 1)) { + return false; + } + if (!args[0].isObject() || !CacheEntry_isCacheEntry(&args[0].toObject())) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected cache entry, got %s", typeName); + return false; + } + RootedObject cacheEntry(cx, &args[0].toObject()); + + UniqueChars fileNameBytes; + CompileOptions options(cx); + options.setIntroductionType("js shell offThreadDecodeStencil") + .setFileAndLine("", 1); + + if (args.length() >= 2) { + if (!args[1].isObject()) { + JS_ReportErrorASCII( + cx, "offThreadDecodeStencil: The 2nd argument must be an object"); + return false; + } + + RootedObject opts(cx, &args[1].toObject()); + if (!js::ParseCompileOptions(cx, options, opts, &fileNameBytes)) { + return false; + } + } + + // This option setting must override whatever the caller requested, and + // this should match `Evaluate` that encodes the script. + options.setIsRunOnce(false); + + // We assume the caller wants caching if at all possible, ignoring + // heuristics that make sense for a real browser. + options.forceAsync = true; + + JS::TranscodeBuffer loadBuffer; + size_t loadLength = 0; + uint8_t* loadData = nullptr; + loadData = CacheEntry_getBytecode(cx, cacheEntry, &loadLength); + if (!loadData) { + return false; + } + if (!loadBuffer.append(loadData, loadLength)) { + JS_ReportOutOfMemory(cx); + return false; + } + + JS::DecodeOptions decodeOptions(options); + if (!JS::CanDecodeOffThread(cx, decodeOptions, loadLength)) { + JS_ReportErrorASCII(cx, "cannot compile code on worker thread"); + return false; + } + + OffThreadJob* job = + NewOffThreadJob(cx, options, OffThreadJob::Source(std::move(loadBuffer))); + if (!job) { + return false; + } + + if (!JS::DecodeStencilOffThread(cx, decodeOptions, job->xdrBuffer(), 0, + OffThreadCompileScriptCallback, job)) { + job->cancel(); + DeleteOffThreadJob(cx, job); + return false; + } + + args.rval().setInt32(job->id); + return true; +} + +class AutoCStringVector { + Vector argv_; + + public: + explicit AutoCStringVector(JSContext* cx) : argv_(cx) {} + ~AutoCStringVector() { + for (size_t i = 0; i < argv_.length(); i++) { + js_free(argv_[i]); + } + } + bool append(UniqueChars&& arg) { + if (!argv_.append(arg.get())) { + return false; + } + + // Now owned by this vector. + (void)arg.release(); + return true; + } + char* const* get() const { return argv_.begin(); } + size_t length() const { return argv_.length(); } + char* operator[](size_t i) const { return argv_[i]; } + void replace(size_t i, UniqueChars arg) { + js_free(argv_[i]); + argv_[i] = arg.release(); + } +}; + +#if defined(XP_WIN) +static bool EscapeForShell(JSContext* cx, AutoCStringVector& argv) { + // Windows will break arguments in argv by various spaces, so we wrap each + // argument in quotes and escape quotes within. Even with quotes, \ will be + // treated like an escape character, so inflate each \ to \\. + + for (size_t i = 0; i < argv.length(); i++) { + if (!argv[i]) { + continue; + } + + size_t newLen = 3; // quotes before and after and null-terminator + for (char* p = argv[i]; *p; p++) { + newLen++; + if (*p == '\"' || *p == '\\') { + newLen++; + } + } + + auto escaped = cx->make_pod_array(newLen); + if (!escaped) { + return false; + } + + char* src = argv[i]; + char* dst = escaped.get(); + *dst++ = '\"'; + while (*src) { + if (*src == '\"' || *src == '\\') { + *dst++ = '\\'; + } + *dst++ = *src++; + } + *dst++ = '\"'; + *dst++ = '\0'; + MOZ_ASSERT(escaped.get() + newLen == dst); + + argv.replace(i, std::move(escaped)); + } + return true; +} +#endif + +#ifndef __wasi__ +static bool ReadAll(int fd, wasm::Bytes* bytes) { + size_t lastLength = bytes->length(); + while (true) { + static const int ChunkSize = 64 * 1024; + if (!bytes->growBy(ChunkSize)) { + return false; + } + + intptr_t readCount; + while (true) { + readCount = read(fd, bytes->begin() + lastLength, ChunkSize); + if (readCount >= 0) { + break; + } + if (errno != EINTR) { + return false; + } + } + + if (readCount < ChunkSize) { + bytes->shrinkTo(lastLength + readCount); + if (readCount == 0) { + return true; + } + } + + lastLength = bytes->length(); + } +} + +static bool WriteAll(int fd, const uint8_t* bytes, size_t length) { + while (length > 0) { + int written = write(fd, bytes, length); + if (written < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + MOZ_ASSERT(unsigned(written) <= length); + length -= written; + bytes += written; + } + + return true; +} + +class AutoPipe { + int fds_[2]; + + public: + AutoPipe() { + fds_[0] = -1; + fds_[1] = -1; + } + + ~AutoPipe() { + if (fds_[0] != -1) { + close(fds_[0]); + } + if (fds_[1] != -1) { + close(fds_[1]); + } + } + + bool init() { +# ifdef XP_WIN + return !_pipe(fds_, 4096, O_BINARY); +# else + return !pipe(fds_); +# endif + } + + int reader() const { + MOZ_ASSERT(fds_[0] != -1); + return fds_[0]; + } + + int writer() const { + MOZ_ASSERT(fds_[1] != -1); + return fds_[1]; + } + + void closeReader() { + MOZ_ASSERT(fds_[0] != -1); + close(fds_[0]); + fds_[0] = -1; + } + + void closeWriter() { + MOZ_ASSERT(fds_[1] != -1); + close(fds_[1]); + fds_[1] = -1; + } +}; +#endif // __wasi__ + +int shell::sArgc; +char** shell::sArgv; + +#ifndef __wasi__ +static const char sWasmCompileAndSerializeFlag[] = + "--wasm-compile-and-serialize"; +static Vector sCompilerProcessFlags; + +static bool CompileAndSerializeInSeparateProcess(JSContext* cx, + const uint8_t* bytecode, + size_t bytecodeLength, + wasm::Bytes* serialized) { + AutoPipe stdIn, stdOut; + if (!stdIn.init() || !stdOut.init()) { + return false; + } + + AutoCStringVector argv(cx); + + UniqueChars argv0 = DuplicateString(cx, sArgv[0]); + if (!argv0 || !argv.append(std::move(argv0))) { + return false; + } + + // Put compiler flags first since they must precede the non-option + // file-descriptor args (passed on Windows, below). + for (unsigned i = 0; i < sCompilerProcessFlags.length(); i++) { + UniqueChars flags = DuplicateString(cx, sCompilerProcessFlags[i]); + if (!flags || !argv.append(std::move(flags))) { + return false; + } + } + + UniqueChars arg; + + arg = DuplicateString(sWasmCompileAndSerializeFlag); + if (!arg || !argv.append(std::move(arg))) { + return false; + } + +# ifdef XP_WIN + // The spawned process will have all the stdIn/stdOut file handles open, but + // without the power of fork, we need some other way to communicate the + // integer fd values so we encode them in argv and WasmCompileAndSerialize() + // has a matching #ifdef XP_WIN to parse them out. Communicate both ends of + // both pipes so the child process can closed the unused ends. + + arg = JS_smprintf("%d", stdIn.reader()); + if (!arg || !argv.append(std::move(arg))) { + return false; + } + + arg = JS_smprintf("%d", stdIn.writer()); + if (!arg || !argv.append(std::move(arg))) { + return false; + } + + arg = JS_smprintf("%d", stdOut.reader()); + if (!arg || !argv.append(std::move(arg))) { + return false; + } + + arg = JS_smprintf("%d", stdOut.writer()); + if (!arg || !argv.append(std::move(arg))) { + return false; + } +# endif + + // Required by both _spawnv and exec. + if (!argv.append(nullptr)) { + return false; + } + +# ifdef XP_WIN + if (!EscapeForShell(cx, argv)) { + return false; + } + + int childPid = _spawnv(P_NOWAIT, sArgv[0], argv.get()); + if (childPid == -1) { + return false; + } +# else + pid_t childPid = fork(); + switch (childPid) { + case -1: + return false; + case 0: + // In the child process. Redirect stdin/stdout to the respective ends of + // the pipes. Closing stdIn.writer() is necessary for stdin to hit EOF. + // This case statement must not return before exec() takes over. Rather, + // exit(-1) is used to return failure to the parent process. + if (dup2(stdIn.reader(), STDIN_FILENO) == -1) { + exit(-1); + } + if (dup2(stdOut.writer(), STDOUT_FILENO) == -1) { + exit(-1); + } + close(stdIn.reader()); + close(stdIn.writer()); + close(stdOut.reader()); + close(stdOut.writer()); + execv(sArgv[0], argv.get()); + exit(-1); + } +# endif + + // In the parent process. Closing stdOut.writer() is necessary for + // stdOut.reader() below to hit EOF. + stdIn.closeReader(); + stdOut.closeWriter(); + + if (!WriteAll(stdIn.writer(), bytecode, bytecodeLength)) { + return false; + } + + stdIn.closeWriter(); + + if (!ReadAll(stdOut.reader(), serialized)) { + return false; + } + + stdOut.closeReader(); + + int status; +# ifdef XP_WIN + if (_cwait(&status, childPid, WAIT_CHILD) == -1) { + return false; + } +# else + while (true) { + if (waitpid(childPid, &status, 0) >= 0) { + break; + } + if (errno != EINTR) { + return false; + } + } +# endif + + return status == 0; +} + +static bool WasmCompileAndSerialize(JSContext* cx) { + MOZ_ASSERT(wasm::CodeCachingAvailable(cx)); + +# ifdef XP_WIN + // See CompileAndSerializeInSeparateProcess for why we've had to smuggle + // these fd values through argv. Closing the writing ends is necessary for + // the reading ends to hit EOF. + int flagIndex = 0; + for (; flagIndex < sArgc; flagIndex++) { + if (!strcmp(sArgv[flagIndex], sWasmCompileAndSerializeFlag)) { + break; + } + } + MOZ_RELEASE_ASSERT(flagIndex < sArgc); + + int fdsIndex = flagIndex + 1; + MOZ_RELEASE_ASSERT(fdsIndex + 4 == sArgc); + + int stdInReader = atoi(sArgv[fdsIndex + 0]); + int stdInWriter = atoi(sArgv[fdsIndex + 1]); + int stdOutReader = atoi(sArgv[fdsIndex + 2]); + int stdOutWriter = atoi(sArgv[fdsIndex + 3]); + + int stdIn = stdInReader; + close(stdInWriter); + close(stdOutReader); + int stdOut = stdOutWriter; +# else + int stdIn = STDIN_FILENO; + int stdOut = STDOUT_FILENO; +# endif + + wasm::MutableBytes bytecode = js_new(); + if (!ReadAll(stdIn, &bytecode->bytes)) { + return false; + } + + wasm::Bytes serialized; + if (!wasm::CompileAndSerialize(cx, *bytecode, &serialized)) { + return false; + } + + if (!WriteAll(stdOut, serialized.begin(), serialized.length())) { + return false; + } + + return true; +} + +static bool WasmCompileInSeparateProcess(JSContext* cx, unsigned argc, + Value* vp) { + if (!wasm::CodeCachingAvailable(cx)) { + JS_ReportErrorASCII(cx, "WebAssembly caching not supported"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "wasmCompileInSeparateProcess", 1)) { + return false; + } + + SharedMem bytecode; + size_t numBytes; + if (!args[0].isObject() || + !IsBufferSource(&args[0].toObject(), &bytecode, &numBytes)) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Argument must be a buffer source"); + return false; + } + + wasm::Bytes serialized; + if (!CompileAndSerializeInSeparateProcess(cx, bytecode.unwrap(), numBytes, + &serialized)) { + if (!cx->isExceptionPending()) { + JS_ReportErrorASCII(cx, "creating and executing child process"); + } + return false; + } + + RootedObject module(cx); + if (!wasm::DeserializeModule(cx, serialized, &module)) { + return false; + } + + args.rval().setObject(*module); + return true; +} +#endif // __wasi__ + +static bool DecompileFunction(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() < 1 || !args[0].isObject() || + !args[0].toObject().is()) { + args.rval().setUndefined(); + return true; + } + RootedFunction fun(cx, &args[0].toObject().as()); + JSString* result = JS_DecompileFunction(cx, fun); + if (!result) { + return false; + } + args.rval().setString(result); + return true; +} + +static bool DecompileThisScript(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + NonBuiltinScriptFrameIter iter(cx); + if (iter.done()) { + args.rval().setString(cx->runtime()->emptyString); + return true; + } + + { + JSAutoRealm ar(cx, iter.script()); + + RootedScript script(cx, iter.script()); + JSString* result = JS_DecompileScript(cx, script); + if (!result) { + return false; + } + + args.rval().setString(result); + } + + return JS_WrapValue(cx, args.rval()); +} + +static bool ValueToSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JSString* str = ValueToSource(cx, args.get(0)); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static bool ThisFilename(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JS::AutoFilename filename; + if (!DescribeScriptedCaller(cx, &filename) || !filename.get()) { + args.rval().setString(cx->runtime()->emptyString); + return true; + } + + JSString* str = NewStringCopyUTF8(cx, filename.get()); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static bool WrapWithProto(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Value obj = args.get(0); + Value proto = args.get(1); + if (!obj.isObject() || !proto.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "wrapWithProto"); + return false; + } + + // Disallow constructing (deeply) nested wrapper chains, to avoid running + // out of stack space in isCallable/isConstructor. See bug 1126105. + if (IsWrapper(&obj.toObject())) { + JS_ReportErrorASCII(cx, "wrapWithProto cannot wrap a wrapper"); + return false; + } + + WrapperOptions options(cx); + options.setProto(proto.toObjectOrNull()); + JSObject* wrapped = Wrapper::New(cx, &obj.toObject(), + &Wrapper::singletonWithPrototype, options); + if (!wrapped) { + return false; + } + + args.rval().setObject(*wrapped); + return true; +} + +static bool NewGlobal(JSContext* cx, unsigned argc, Value* vp) { + JS::RealmOptions options; + JS::RealmCreationOptions& creationOptions = options.creationOptions(); + JS::RealmBehaviors& behaviors = options.behaviors(); + ShellGlobalKind kind = ShellGlobalKind::WindowProxy; + bool immutablePrototype = true; + + SetStandardRealmOptions(options); + + // Default to creating the global in the current compartment unless + // --more-compartments is used. + if (defaultToSameCompartment) { + creationOptions.setExistingCompartment(cx->global()); + } else { + creationOptions.setNewCompartmentAndZone(); + } + + JS::AutoHoldPrincipals principals(cx); + + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 1 && args[0].isObject()) { + RootedObject opts(cx, &args[0].toObject()); + RootedValue v(cx); + + if (!JS_GetProperty(cx, opts, "invisibleToDebugger", &v)) { + return false; + } + if (v.isBoolean()) { + creationOptions.setInvisibleToDebugger(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "sameZoneAs", &v)) { + return false; + } + if (v.isObject()) { + creationOptions.setNewCompartmentInExistingZone( + UncheckedUnwrap(&v.toObject())); + } + + if (!JS_GetProperty(cx, opts, "sameCompartmentAs", &v)) { + return false; + } + if (v.isObject()) { + creationOptions.setExistingCompartment(UncheckedUnwrap(&v.toObject())); + } + + if (!JS_GetProperty(cx, opts, "newCompartment", &v)) { + return false; + } + if (v.isBoolean() && v.toBoolean()) { + creationOptions.setNewCompartmentAndZone(); + } + + if (!JS_GetProperty(cx, opts, "discardSource", &v)) { + return false; + } + if (v.isBoolean()) { + behaviors.setDiscardSource(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "useWindowProxy", &v)) { + return false; + } + if (v.isBoolean()) { + kind = v.toBoolean() ? ShellGlobalKind::WindowProxy + : ShellGlobalKind::GlobalObject; + } + + if (!JS_GetProperty(cx, opts, "immutablePrototype", &v)) { + return false; + } + if (v.isBoolean()) { + immutablePrototype = v.toBoolean(); + } + + if (!JS_GetProperty(cx, opts, "systemPrincipal", &v)) { + return false; + } + if (v.isBoolean()) { + principals.reset(&ShellPrincipals::fullyTrusted); + } + + if (!JS_GetProperty(cx, opts, "principal", &v)) { + return false; + } + if (!v.isUndefined()) { + uint32_t bits; + if (!ToUint32(cx, v, &bits)) { + return false; + } + JSPrincipals* newPrincipals = cx->new_(bits); + if (!newPrincipals) { + return false; + } + principals.reset(newPrincipals); + } + + if (!JS_GetProperty(cx, opts, "enableCoopAndCoep", &v)) { + return false; + } + if (v.isBoolean()) { + creationOptions.setCoopAndCoepEnabled(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "freezeBuiltins", &v)) { + return false; + } + if (v.isBoolean()) { + creationOptions.setFreezeBuiltins(v.toBoolean()); + } + + // On the web, the SharedArrayBuffer constructor is not installed as a + // global property in pages that aren't isolated in a separate process (and + // thus can't allow the structured cloning of shared memory). Specify false + // for this option to reproduce this behavior. + if (!JS_GetProperty(cx, opts, "defineSharedArrayBufferConstructor", &v)) { + return false; + } + if (v.isBoolean()) { + creationOptions.setDefineSharedArrayBufferConstructor(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "shouldResistFingerprinting", &v)) { + return false; + } + if (v.isBoolean()) { + behaviors.setShouldResistFingerprinting(v.toBoolean()); + } + } + + if (!CheckRealmOptions(cx, options, principals.get())) { + return false; + } + + RootedObject global(cx, NewGlobalObject(cx, options, principals.get(), kind, + immutablePrototype)); + if (!global) { + return false; + } + + RootedObject wrapped(cx, ToWindowProxyIfWindow(global)); + if (!JS_WrapObject(cx, &wrapped)) { + return false; + } + + args.rval().setObject(*wrapped); + return true; +} + +static bool NukeAllCCWs(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 0) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "nukeAllCCWs"); + return false; + } + + NukeCrossCompartmentWrappers(cx, AllCompartments(), cx->realm(), + NukeWindowReferences, NukeAllReferences); + args.rval().setUndefined(); + return true; +} + +static bool RecomputeWrappers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 2) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "recomputeWrappers"); + return false; + } + + JS::Compartment* sourceComp = nullptr; + if (args.get(0).isObject()) { + sourceComp = JS::GetCompartment(UncheckedUnwrap(&args[0].toObject())); + } + + JS::Compartment* targetComp = nullptr; + if (args.get(1).isObject()) { + targetComp = JS::GetCompartment(UncheckedUnwrap(&args[1].toObject())); + } + + struct SingleOrAllCompartments final : public CompartmentFilter { + JS::Compartment* comp; + explicit SingleOrAllCompartments(JS::Compartment* c) : comp(c) {} + virtual bool match(JS::Compartment* c) const override { + return !comp || comp == c; + } + }; + + if (!js::RecomputeWrappers(cx, SingleOrAllCompartments(sourceComp), + SingleOrAllCompartments(targetComp))) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool DumpObjectWrappers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + bool printedHeader = false; + for (ZonesIter zone(cx->runtime(), WithAtoms); !zone.done(); zone.next()) { + bool printedZoneInfo = false; + for (CompartmentsInZoneIter comp(zone); !comp.done(); comp.next()) { + bool printedCompartmentInfo = false; + for (Compartment::ObjectWrapperEnum e(comp); !e.empty(); e.popFront()) { + JSObject* wrapper = e.front().value().unbarrieredGet(); + JSObject* wrapped = e.front().key(); + if (!printedHeader) { + fprintf(stderr, "Cross-compartment object wrappers:\n"); + printedHeader = true; + } + if (!printedZoneInfo) { + fprintf(stderr, " Zone %p:\n", zone.get()); + printedZoneInfo = true; + } + if (!printedCompartmentInfo) { + fprintf(stderr, " Compartment %p:\n", comp.get()); + printedCompartmentInfo = true; + } + fprintf(stderr, + " Object wrapper %p -> %p in zone %p compartment %p\n", + wrapper, wrapped, wrapped->zone(), wrapped->compartment()); + } + } + } + + if (!printedHeader) { + fprintf(stderr, "No cross-compartment object wrappers.\n"); + } + + args.rval().setUndefined(); + return true; +} + +static bool GetMaxArgs(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setInt32(ARGS_LENGTH_MAX); + return true; +} + +static bool IsHTMLDDA_Call(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // These are the required conditions under which this object may be called + // by test262 tests, and the required behavior under those conditions. + if (args.length() == 0 || + (args[0].isString() && args[0].toString()->length() == 0)) { + args.rval().setNull(); + return true; + } + + JS_ReportErrorASCII( + cx, "IsHTMLDDA object is being called in an impermissible manner"); + return false; +} + +static bool CreateIsHTMLDDA(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + static const JSClassOps classOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + IsHTMLDDA_Call, // call + nullptr, // construct + nullptr, // trace + }; + + static const JSClass cls = { + "IsHTMLDDA", + JSCLASS_EMULATES_UNDEFINED, + &classOps, + }; + + JSObject* obj = JS_NewObject(cx, &cls); + if (!obj) { + return false; + } + args.rval().setObject(*obj); + return true; +} + +static bool GetSelfHostedValue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isString()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "getSelfHostedValue"); + return false; + } + Rooted srcAtom(cx, ToAtom(cx, args[0])); + if (!srcAtom) { + return false; + } + Rooted srcName(cx, srcAtom->asPropertyName()); + return GlobalObject::getIntrinsicValue(cx, cx->global(), srcName, + args.rval()); +} + +class ShellSourceHook : public SourceHook { + // The function we should call to lazily retrieve source code. + PersistentRootedFunction fun; + + public: + ShellSourceHook(JSContext* cx, JSFunction& fun) : fun(cx, &fun) {} + + bool load(JSContext* cx, const char* filename, char16_t** twoByteSource, + char** utf8Source, size_t* length) override { + MOZ_ASSERT((twoByteSource != nullptr) != (utf8Source != nullptr), + "must be called requesting only one of UTF-8 or UTF-16 source"); + + RootedString str(cx); + if (filename) { + str = NewStringCopyUTF8(cx, filename); + if (!str) { + return false; + } + } else { + str = JS_GetEmptyString(cx); + } + RootedValue filenameValue(cx, StringValue(str)); + + RootedValue result(cx); + if (!Call(cx, UndefinedHandleValue, fun, HandleValueArray(filenameValue), + &result)) { + return false; + } + + str = JS::ToString(cx, result); + if (!str) { + return false; + } + + Rooted linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + if (twoByteSource) { + *length = JS_GetStringLength(linear); + + *twoByteSource = cx->pod_malloc(*length); + if (!*twoByteSource) { + return false; + } + + CopyChars(*twoByteSource, *linear); + } else { + MOZ_ASSERT(utf8Source != nullptr); + + *length = JS::GetDeflatedUTF8StringLength(linear); + + *utf8Source = cx->pod_malloc(*length); + if (!*utf8Source) { + return false; + } + + mozilla::DebugOnly dstLen = JS::DeflateStringToUTF8Buffer( + linear, mozilla::Span(*utf8Source, *length)); + MOZ_ASSERT(dstLen == *length); + } + + return true; + } +}; + +static bool WithSourceHook(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 2) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments."); + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is() || + !args[1].isObject() || !args[1].toObject().is()) { + ReportUsageErrorASCII(cx, callee, + "First and second arguments must be functions."); + return false; + } + + mozilla::UniquePtr hook = + mozilla::MakeUnique(cx, + args[0].toObject().as()); + if (!hook) { + return false; + } + + mozilla::UniquePtr savedHook = js::ForgetSourceHook(cx); + js::SetSourceHook(cx, std::move(hook)); + + RootedObject fun(cx, &args[1].toObject()); + bool result = Call(cx, UndefinedHandleValue, fun, + JS::HandleValueArray::empty(), args.rval()); + js::SetSourceHook(cx, std::move(savedHook)); + return result; +} + +static void PrintProfilerEvents_Callback(const char* msg, const char* details) { + fprintf(stderr, "PROFILER EVENT: %s %s\n", msg, details); +} + +static bool PrintProfilerEvents(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (cx->runtime()->geckoProfiler().enabled()) { + js::RegisterContextProfilingEventMarker(cx, &PrintProfilerEvents_Callback); + } + args.rval().setUndefined(); + return true; +} + +#ifdef SINGLESTEP_PROFILING +static void SingleStepCallback(void* arg, jit::Simulator* sim, void* pc) { + JSContext* cx = reinterpret_cast(arg); + + // If profiling is not enabled, don't do anything. + if (!cx->runtime()->geckoProfiler().enabled()) { + return; + } + + JS::ProfilingFrameIterator::RegisterState state; + state.pc = pc; +# if defined(JS_SIMULATOR_ARM) + state.sp = (void*)sim->get_register(jit::Simulator::sp); + state.lr = (void*)sim->get_register(jit::Simulator::lr); + state.fp = (void*)sim->get_register(jit::Simulator::fp); +# elif defined(JS_SIMULATOR_MIPS64) || defined(JS_SIMULATOR_MIPS32) + state.sp = (void*)sim->getRegister(jit::Simulator::sp); + state.lr = (void*)sim->getRegister(jit::Simulator::ra); + state.fp = (void*)sim->getRegister(jit::Simulator::fp); +# elif defined(JS_SIMULATOR_LOONG64) + state.sp = (void*)sim->getRegister(jit::Simulator::sp); + state.lr = (void*)sim->getRegister(jit::Simulator::ra); + state.fp = (void*)sim->getRegister(jit::Simulator::fp); +# else +# error "NYI: Single-step profiling support" +# endif + + mozilla::DebugOnly lastStackAddress = nullptr; + StackChars stack; + uint32_t frameNo = 0; + AutoEnterOOMUnsafeRegion oomUnsafe; + for (JS::ProfilingFrameIterator i(cx, state); !i.done(); ++i) { + MOZ_ASSERT(i.stackAddress() != nullptr); + MOZ_ASSERT(lastStackAddress <= i.stackAddress()); + lastStackAddress = i.stackAddress(); + JS::ProfilingFrameIterator::Frame frames[16]; + uint32_t nframes = i.extractStack(frames, 0, 16); + for (uint32_t i = 0; i < nframes; i++) { + // Assert endStackAddress never exceeds sp (bug 1782188). + MOZ_ASSERT(frames[i].endStackAddress >= state.sp); + if (frameNo > 0) { + if (!stack.append(",", 1)) { + oomUnsafe.crash("stack.append"); + } + } + if (!stack.append(frames[i].label, strlen(frames[i].label))) { + oomUnsafe.crash("stack.append"); + } + frameNo++; + } + } + + ShellContext* sc = GetShellContext(cx); + + // Only append the stack if it differs from the last stack. + if (sc->stacks.empty() || sc->stacks.back().length() != stack.length() || + !ArrayEqual(sc->stacks.back().begin(), stack.begin(), stack.length())) { + if (!sc->stacks.append(std::move(stack))) { + oomUnsafe.crash("stacks.append"); + } + } +} +#endif + +static bool EnableSingleStepProfiling(JSContext* cx, unsigned argc, Value* vp) { +#ifdef SINGLESTEP_PROFILING + CallArgs args = CallArgsFromVp(argc, vp); + + jit::Simulator* sim = cx->simulator(); + sim->enable_single_stepping(SingleStepCallback, cx); + + args.rval().setUndefined(); + return true; +#else + JS_ReportErrorASCII(cx, "single-step profiling not enabled on this platform"); + return false; +#endif +} + +static bool DisableSingleStepProfiling(JSContext* cx, unsigned argc, + Value* vp) { +#ifdef SINGLESTEP_PROFILING + CallArgs args = CallArgsFromVp(argc, vp); + + jit::Simulator* sim = cx->simulator(); + sim->disable_single_stepping(); + + ShellContext* sc = GetShellContext(cx); + + RootedValueVector elems(cx); + for (size_t i = 0; i < sc->stacks.length(); i++) { + JSString* stack = + JS_NewUCStringCopyN(cx, sc->stacks[i].begin(), sc->stacks[i].length()); + if (!stack) { + return false; + } + if (!elems.append(StringValue(stack))) { + return false; + } + } + + JSObject* array = JS::NewArrayObject(cx, elems); + if (!array) { + return false; + } + + sc->stacks.clear(); + args.rval().setObject(*array); + return true; +#else + JS_ReportErrorASCII(cx, "single-step profiling not enabled on this platform"); + return false; +#endif +} + +static bool IsLatin1(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + bool isLatin1 = + args.get(0).isString() && args[0].toString()->hasLatin1Chars(); + args.rval().setBoolean(isLatin1); + return true; +} + +static bool EnableGeckoProfiling(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!EnsureGeckoProfilingStackInstalled(cx, GetShellContext(cx))) { + return false; + } + + cx->runtime()->geckoProfiler().enableSlowAssertions(false); + cx->runtime()->geckoProfiler().enable(true); + + args.rval().setUndefined(); + return true; +} + +static bool EnableGeckoProfilingWithSlowAssertions(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + + if (cx->runtime()->geckoProfiler().enabled()) { + // If profiling already enabled with slow assertions disabled, + // this is a no-op. + if (cx->runtime()->geckoProfiler().slowAssertionsEnabled()) { + return true; + } + + // Slow assertions are off. Disable profiling before re-enabling + // with slow assertions on. + cx->runtime()->geckoProfiler().enable(false); + } + + if (!EnsureGeckoProfilingStackInstalled(cx, GetShellContext(cx))) { + return false; + } + + cx->runtime()->geckoProfiler().enableSlowAssertions(true); + cx->runtime()->geckoProfiler().enable(true); + + return true; +} + +static bool DisableGeckoProfiling(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + + if (!cx->runtime()->geckoProfiler().enabled()) { + return true; + } + + cx->runtime()->geckoProfiler().enable(false); + return true; +} + +// Global mailbox that is used to communicate a shareable object value from one +// worker to another. +// +// These object types are shareable: +// +// - SharedArrayBuffer +// - WasmMemoryObject (when constructed with shared:true) +// - WasmModuleObject +// +// For the SharedArrayBuffer and WasmMemoryObject we transmit the underlying +// SharedArrayRawBuffer ("SARB"). For the WasmModuleObject we transmit the +// underlying JS::WasmModule. The transmitted types are refcounted. When they +// are in the mailbox their reference counts are at least 1, accounting for the +// reference from the mailbox. +// +// The lock guards the mailbox variable and prevents a race where two workers +// try to set the mailbox at the same time to replace an object that is only +// referenced from the mailbox: the workers will both decrement the reference +// count on the old object, and one of those decrements will be on a garbage +// object. We could implement this with atomics and a CAS loop but it's not +// worth the bother. +// +// Note that if a thread reads the mailbox repeatedly it will get distinct +// objects on each read. The alternatives are to cache created objects locally, +// but this retains storage we don't need to retain, or to somehow clear the +// mailbox locally, but this creates a coordination headache. Buyer beware. + +enum class MailboxTag { + Empty, + SharedArrayBuffer, + WasmMemory, + WasmModule, + Number, +}; + +struct SharedObjectMailbox { + union Value { + struct { + SharedArrayRawBuffer* buffer; + size_t length; + bool isHugeMemory; // For a WasmMemory tag, otherwise false + } sarb; + JS::WasmModule* module; + double number; + + Value() : number(0.0) {} + }; + + MailboxTag tag = MailboxTag::Empty; + Value val; +}; + +typedef ExclusiveData SOMailbox; + +// Never null after successful initialization. +static SOMailbox* sharedObjectMailbox; + +static bool InitSharedObjectMailbox() { + sharedObjectMailbox = js_new(mutexid::ShellObjectMailbox); + return sharedObjectMailbox != nullptr; +} + +static void DestructSharedObjectMailbox() { + // All workers need to have terminated at this point. + + { + auto mbx = sharedObjectMailbox->lock(); + switch (mbx->tag) { + case MailboxTag::Empty: + case MailboxTag::Number: + break; + case MailboxTag::SharedArrayBuffer: + case MailboxTag::WasmMemory: + mbx->val.sarb.buffer->dropReference(); + break; + case MailboxTag::WasmModule: + mbx->val.module->Release(); + break; + default: + MOZ_CRASH(); + } + } + + js_delete(sharedObjectMailbox); + sharedObjectMailbox = nullptr; +} + +static bool GetSharedObject(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject newObj(cx); + + { + auto mbx = sharedObjectMailbox->lock(); + switch (mbx->tag) { + case MailboxTag::Empty: { + break; + } + case MailboxTag::Number: { + args.rval().setNumber(mbx->val.number); + return true; + } + case MailboxTag::SharedArrayBuffer: + case MailboxTag::WasmMemory: { + // Flag was set in the sender; ensure it is set in the receiver. + MOZ_ASSERT( + cx->realm()->creationOptions().getSharedMemoryAndAtomicsEnabled()); + + // The protocol for creating a SAB requires the refcount to be + // incremented prior to the SAB creation. + + SharedArrayRawBuffer* buf = mbx->val.sarb.buffer; + size_t length = mbx->val.sarb.length; + if (!buf->addReference()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SC_SAB_REFCNT_OFLO); + return false; + } + + // If the allocation fails we must decrement the refcount before + // returning. + + Rooted maybesab( + cx, SharedArrayBufferObject::New(cx, buf, length)); + if (!maybesab) { + buf->dropReference(); + return false; + } + + // At this point the SAB was created successfully and it owns the + // refcount-increase on the buffer that we performed above. So even + // if we fail to allocate along any path below we must not decrement + // the refcount; the garbage collector must be allowed to handle + // that via finalization of the orphaned SAB object. + + if (mbx->tag == MailboxTag::SharedArrayBuffer) { + newObj = maybesab; + } else { + if (!GlobalObject::ensureConstructor(cx, cx->global(), + JSProto_WebAssembly)) { + return false; + } + RootedObject proto(cx, + &cx->global()->getPrototype(JSProto_WasmMemory)); + newObj = WasmMemoryObject::create(cx, maybesab, + mbx->val.sarb.isHugeMemory, proto); + MOZ_ASSERT_IF(newObj, newObj->as().isShared()); + if (!newObj) { + return false; + } + } + + break; + } + case MailboxTag::WasmModule: { + // Flag was set in the sender; ensure it is set in the receiver. + MOZ_ASSERT( + cx->realm()->creationOptions().getSharedMemoryAndAtomicsEnabled()); + + if (!GlobalObject::ensureConstructor(cx, cx->global(), + JSProto_WebAssembly)) { + return false; + } + + // WasmModuleObject::create() increments the refcount on the module + // and signals an error and returns null if that fails. + newObj = mbx->val.module->createObject(cx); + if (!newObj) { + return false; + } + break; + } + default: { + MOZ_CRASH(); + } + } + } + + args.rval().setObjectOrNull(newObj); + return true; +} + +static bool SetSharedObject(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + MailboxTag tag = MailboxTag::Empty; + SharedObjectMailbox::Value value; + + // Increase refcounts when we obtain the value to avoid operating on dead + // storage during self-assignment. + + if (args.get(0).isObject()) { + RootedObject obj(cx, &args[0].toObject()); + if (obj->is()) { + Rooted sab(cx, + &obj->as()); + tag = MailboxTag::SharedArrayBuffer; + value.sarb.buffer = sab->rawBufferObject(); + value.sarb.length = sab->byteLength(); + value.sarb.isHugeMemory = false; + if (!value.sarb.buffer->addReference()) { + JS_ReportErrorASCII(cx, + "Reference count overflow on SharedArrayBuffer"); + return false; + } + } else if (obj->is()) { + // Here we must transmit sab.byteLength() as the length; the SARB has its + // own notion of the length which may be greater, and that's fine. + if (obj->as().isShared()) { + Rooted sab( + cx, &obj->as() + .buffer() + .as()); + tag = MailboxTag::WasmMemory; + value.sarb.buffer = sab->rawBufferObject(); + value.sarb.length = sab->byteLength(); + value.sarb.isHugeMemory = obj->as().isHuge(); + if (!value.sarb.buffer->addReference()) { + JS_ReportErrorASCII(cx, + "Reference count overflow on SharedArrayBuffer"); + return false; + } + } else { + JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject"); + return false; + } + } else if (JS::IsWasmModuleObject(obj)) { + tag = MailboxTag::WasmModule; + value.module = JS::GetWasmModule(obj).forget().take(); + } else { + JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject"); + return false; + } + } else if (args.get(0).isNumber()) { + tag = MailboxTag::Number; + value.number = args.get(0).toNumber(); + // Nothing + } else if (args.get(0).isNullOrUndefined()) { + // Nothing + } else { + JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject"); + return false; + } + + { + auto mbx = sharedObjectMailbox->lock(); + + switch (mbx->tag) { + case MailboxTag::Empty: + case MailboxTag::Number: + break; + case MailboxTag::SharedArrayBuffer: + case MailboxTag::WasmMemory: + mbx->val.sarb.buffer->dropReference(); + break; + case MailboxTag::WasmModule: + mbx->val.module->Release(); + break; + default: + MOZ_CRASH(); + } + + mbx->tag = tag; + mbx->val = value; + } + + args.rval().setUndefined(); + return true; +} + +typedef Vector Uint8Vector; + +class StreamCacheEntry : public AtomicRefCounted, + public JS::OptimizedEncodingListener { + typedef AtomicRefCounted AtomicBase; + + Uint8Vector bytes_; + ExclusiveData optimized_; + + public: + explicit StreamCacheEntry(Uint8Vector&& original) + : bytes_(std::move(original)), + optimized_(mutexid::ShellStreamCacheEntryState) {} + + // Implement JS::OptimizedEncodingListener: + + MozExternalRefCountType MOZ_XPCOM_ABI AddRef() override { + AtomicBase::AddRef(); + return 1; // unused + } + MozExternalRefCountType MOZ_XPCOM_ABI Release() override { + AtomicBase::Release(); + return 0; // unused + } + + const Uint8Vector& bytes() const { return bytes_; } + + void storeOptimizedEncoding(const uint8_t* srcBytes, + size_t srcLength) override { + MOZ_ASSERT(srcLength > 0); + + // Tolerate races since a single StreamCacheEntry object can be used as + // the source of multiple streaming compilations. + auto dstBytes = optimized_.lock(); + if (dstBytes->length() > 0) { + return; + } + + if (!dstBytes->resize(srcLength)) { + return; + } + memcpy(dstBytes->begin(), srcBytes, srcLength); + } + + bool hasOptimizedEncoding() const { return !optimized_.lock()->empty(); } + const Uint8Vector& optimizedEncoding() const { + return optimized_.lock().get(); + } +}; + +typedef RefPtr StreamCacheEntryPtr; + +class StreamCacheEntryObject : public NativeObject { + static const unsigned CACHE_ENTRY_SLOT = 0; + static const JSClassOps classOps_; + static const JSPropertySpec properties_; + + static void finalize(JS::GCContext* gcx, JSObject* obj) { + obj->as().cache().Release(); + } + + static bool cachedGetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.thisv().isObject() || + !args.thisv().toObject().is()) { + return false; + } + + StreamCacheEntryObject& obj = + args.thisv().toObject().as(); + args.rval().setBoolean(obj.cache().hasOptimizedEncoding()); + return true; + } + static bool getBuffer(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.thisv().isObject() || + !args.thisv().toObject().is()) { + return false; + } + + auto& bytes = + args.thisv().toObject().as().cache().bytes(); + RootedArrayBufferObject buffer( + cx, ArrayBufferObject::createZeroed(cx, bytes.length())); + if (!buffer) { + return false; + } + + memcpy(buffer->dataPointer(), bytes.begin(), bytes.length()); + + args.rval().setObject(*buffer); + return true; + } + + public: + static const unsigned RESERVED_SLOTS = 1; + static const JSClass class_; + static const JSPropertySpec properties[]; + + static bool construct(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "streamCacheEntry", 1)) { + return false; + } + + SharedMem ptr; + size_t numBytes; + if (!args[0].isObject() || + !IsBufferSource(&args[0].toObject(), &ptr, &numBytes)) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Argument must be an ArrayBuffer"); + return false; + } + + Uint8Vector bytes; + if (!bytes.resize(numBytes)) { + return false; + } + + memcpy(bytes.begin(), ptr.unwrap(), numBytes); + + RefPtr cache = + cx->new_(std::move(bytes)); + if (!cache) { + return false; + } + + Rooted obj( + cx, NewObjectWithGivenProto(cx, nullptr)); + if (!obj) { + return false; + } + obj->initReservedSlot(CACHE_ENTRY_SLOT, + PrivateValue(cache.forget().take())); + + if (!JS_DefineProperty(cx, obj, "cached", cachedGetter, nullptr, 0)) { + return false; + } + if (!JS_DefineFunction(cx, obj, "getBuffer", getBuffer, 0, 0)) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + StreamCacheEntry& cache() const { + return *(StreamCacheEntry*)getReservedSlot(CACHE_ENTRY_SLOT).toPrivate(); + } +}; + +const JSClassOps StreamCacheEntryObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + StreamCacheEntryObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass StreamCacheEntryObject::class_ = { + "StreamCacheEntryObject", + JSCLASS_HAS_RESERVED_SLOTS(StreamCacheEntryObject::RESERVED_SLOTS) | + JSCLASS_BACKGROUND_FINALIZE, + &StreamCacheEntryObject::classOps_}; + +struct BufferStreamJob { + Variant source; + Thread thread; + JS::StreamConsumer* consumer; + + BufferStreamJob(Uint8Vector&& source, JS::StreamConsumer* consumer) + : source(AsVariant(std::move(source))), consumer(consumer) {} + BufferStreamJob(StreamCacheEntry& source, JS::StreamConsumer* consumer) + : source(AsVariant(&source)), consumer(consumer) {} +}; + +struct BufferStreamState { + Vector, 0, SystemAllocPolicy> jobs; + size_t delayMillis; + size_t chunkSize; + bool shutdown; + + BufferStreamState() : delayMillis(1), chunkSize(10), shutdown(false) {} + + ~BufferStreamState() { MOZ_ASSERT(jobs.empty()); } +}; + +static ExclusiveWaitableData* bufferStreamState; + +static void BufferStreamMain(BufferStreamJob* job) { + const uint8_t* bytes; + size_t byteLength; + JS::OptimizedEncodingListener* listener; + if (job->source.is()) { + StreamCacheEntry& cache = *job->source.as(); + if (cache.hasOptimizedEncoding()) { + const Uint8Vector& optimized = cache.optimizedEncoding(); + job->consumer->consumeOptimizedEncoding(optimized.begin(), + optimized.length()); + goto done; + } + + bytes = cache.bytes().begin(); + byteLength = cache.bytes().length(); + listener = &cache; + } else { + bytes = job->source.as().begin(); + byteLength = job->source.as().length(); + listener = nullptr; + } + + size_t byteOffset; + byteOffset = 0; + while (true) { + if (byteOffset == byteLength) { + job->consumer->streamEnd(listener); + break; + } + + bool shutdown; + size_t delayMillis; + size_t chunkSize; + { + auto state = bufferStreamState->lock(); + shutdown = state->shutdown; + delayMillis = state->delayMillis; + chunkSize = state->chunkSize; + } + + if (shutdown) { + job->consumer->streamError(JSMSG_STREAM_CONSUME_ERROR); + break; + } + + ThisThread::SleepMilliseconds(delayMillis); + + chunkSize = std::min(chunkSize, byteLength - byteOffset); + + if (!job->consumer->consumeChunk(bytes + byteOffset, chunkSize)) { + break; + } + + byteOffset += chunkSize; + } + +done: + auto state = bufferStreamState->lock(); + size_t jobIndex = 0; + while (state->jobs[jobIndex].get() != job) { + jobIndex++; + } + job->thread.detach(); // quiet assert in ~Thread() called by erase(). + state->jobs.erase(state->jobs.begin() + jobIndex); + if (state->jobs.empty()) { + state.notify_all(/* jobs empty */); + } +} + +static bool ConsumeBufferSource(JSContext* cx, JS::HandleObject obj, + JS::MimeType, JS::StreamConsumer* consumer) { + { + RootedValue url(cx); + if (!JS_GetProperty(cx, obj, "url", &url)) { + return false; + } + UniqueChars urlChars; + if (url.isString()) { + Rooted str(cx, url.toString()); + urlChars = JS_EncodeStringToUTF8(cx, str); + if (!urlChars) { + return false; + } + } + + RootedValue mapUrl(cx); + if (!JS_GetProperty(cx, obj, "sourceMappingURL", &mapUrl)) { + return false; + } + UniqueChars mapUrlChars; + if (mapUrl.isString()) { + Rooted str(cx, mapUrl.toString()); + mapUrlChars = JS_EncodeStringToUTF8(cx, str); + if (!mapUrlChars) { + return false; + } + } + + consumer->noteResponseURLs(urlChars.get(), mapUrlChars.get()); + } + + UniquePtr job; + + SharedMem dataPointer; + size_t byteLength; + if (IsBufferSource(obj, &dataPointer, &byteLength)) { + Uint8Vector bytes; + if (!bytes.resize(byteLength)) { + JS_ReportOutOfMemory(cx); + return false; + } + + memcpy(bytes.begin(), dataPointer.unwrap(), byteLength); + job = cx->make_unique(std::move(bytes), consumer); + } else if (obj->is()) { + job = cx->make_unique( + obj->as().cache(), consumer); + } else { + JS_ReportErrorASCII( + cx, + "shell streaming consumes a buffer source (buffer or view) " + "or StreamCacheEntryObject"); + return false; + } + if (!job) { + return false; + } + + BufferStreamJob* jobPtr = job.get(); + + { + auto state = bufferStreamState->lock(); + MOZ_ASSERT(!state->shutdown); + if (!state->jobs.append(std::move(job))) { + JS_ReportOutOfMemory(cx); + return false; + } + } + + { + AutoEnterOOMUnsafeRegion oomUnsafe; + if (!jobPtr->thread.init(BufferStreamMain, jobPtr)) { + oomUnsafe.crash("ConsumeBufferSource"); + } + } + + return true; +} + +static void ReportStreamError(JSContext* cx, size_t errorNumber) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, errorNumber); +} + +static bool SetBufferStreamParams(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "setBufferStreamParams", 2)) { + return false; + } + + double delayMillis; + if (!ToNumber(cx, args[0], &delayMillis)) { + return false; + } + + double chunkSize; + if (!ToNumber(cx, args[1], &chunkSize)) { + return false; + } + + { + auto state = bufferStreamState->lock(); + state->delayMillis = delayMillis; + state->chunkSize = chunkSize; + } + + args.rval().setUndefined(); + return true; +} + +static void ShutdownBufferStreams() { + auto state = bufferStreamState->lock(); + state->shutdown = true; + while (!state->jobs.empty()) { + state.wait(/* jobs empty */); + } + state->jobs.clearAndFree(); +} + +static bool DumpScopeChain(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (js::SupportDifferentialTesting()) { + ReportUsageErrorASCII( + cx, callee, "Function not available in differential testing mode."); + return false; + } + + if (args.length() != 1) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + if (!args[0].isObject() || + !(args[0].toObject().is() || + args[0].toObject().is())) { + ReportUsageErrorASCII( + cx, callee, "Argument must be an interpreted function or a module"); + return false; + } + + RootedObject obj(cx, &args[0].toObject()); + RootedScript script(cx); + + if (obj->is()) { + RootedFunction fun(cx, &obj->as()); + if (!fun->isInterpreted()) { + ReportUsageErrorASCII(cx, callee, + "Argument must be an interpreted function"); + return false; + } + script = JSFunction::getOrCreateScript(cx, fun); + if (!script) { + return false; + } + } else { + script = obj->as().get()->maybeScript(); + if (!script) { + JS_ReportErrorASCII(cx, "module does not have an associated script"); + return false; + } + } + + script->bodyScope()->dump(); + + args.rval().setUndefined(); + return true; +} + +// For testing GC marking, blackRoot() and grayRoot() will heap-allocate an +// array whose elements (as well as the array itself) will be marked as roots in +// subsequent GCs. +// +// Note that EnsureGrayRoot() will blacken the returned object, so it will not +// actually end up marked gray until the following GC clears the black bit +// (assuming nothing is holding onto it.) +// +// The idea is that you can set up a whole graph of objects to be marked gray, +// hanging off of the object returned from grayRoot(). Then you GC to clear the +// black bits and set the gray bits. +// +// To test grayness, register the objects of interest with addMarkObservers(), +// which takes an Array of objects (which will be marked black at the time +// they're passed in). Their mark bits may be retrieved at any time with +// getMarks(), in the form of an array of strings with each index corresponding +// to the original objects passed to addMarkObservers(). + +static bool EnsureRootArray(JSContext* cx, gc::MarkColor color, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + auto priv = EnsureShellCompartmentPrivate(cx); + if (!priv) { + return false; + } + + GCPtr& root = + (color == gc::MarkColor::Black) ? priv->blackRoot : priv->grayRoot; + + if (!root && !(root = NewTenuredDenseEmptyArray(cx))) { + return false; + } + + // Barrier to enforce the invariant that JS does not touch gray objects. + JSObject* obj = root; + JS::ExposeObjectToActiveJS(obj); + + args.rval().setObject(*obj); + return true; +} + +static bool EnsureBlackRoot(JSContext* cx, unsigned argc, Value* vp) { + return EnsureRootArray(cx, gc::MarkColor::Black, argc, vp); +} + +static bool EnsureGrayRoot(JSContext* cx, unsigned argc, Value* vp) { + return EnsureRootArray(cx, gc::MarkColor::Gray, argc, vp); +} + +static MarkBitObservers* EnsureMarkBitObservers(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + if (!sc->markObservers) { + auto* observers = + cx->new_(cx->runtime(), NonshrinkingGCObjectVector()); + if (!observers) { + return nullptr; + } + sc->markObservers.reset(observers); + } + return sc->markObservers.get(); +} + +static bool ClearMarkObservers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + auto markObservers = EnsureMarkBitObservers(cx); + if (!markObservers) { + return false; + } + + markObservers->get().clear(); + + args.rval().setUndefined(); + return true; +} + +static bool AddMarkObservers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + auto markObservers = EnsureMarkBitObservers(cx); + if (!markObservers) { + return false; + } + + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "argument must be an Array of objects"); + return false; + } + + RootedObject observersArg(cx, &args[0].toObject()); + uint64_t length; + if (!GetLengthProperty(cx, observersArg, &length)) { + return false; + } + + if (length > UINT32_MAX) { + JS_ReportErrorASCII(cx, "Invalid length for observers array"); + return false; + } + + RootedValue value(cx); + RootedObject object(cx); + for (uint32_t i = 0; i < length; i++) { + if (!JS_GetElement(cx, observersArg, i, &value)) { + return false; + } + + if (!value.isObject()) { + JS_ReportErrorASCII(cx, "argument must be an Array of objects"); + return false; + } + + object = &value.toObject(); + if (gc::IsInsideNursery(object)) { + // WeakCaches are not swept during a minor GC. To prevent + // nursery-allocated contents from having the mark bits be deceptively + // black until the second GC, they would need to be marked weakly (cf + // NurseryAwareHashMap). It is simpler to evict the nursery to prevent + // nursery objects from being observed. + cx->runtime()->gc.evictNursery(); + } + + if (!markObservers->get().append(object)) { + return false; + } + } + + args.rval().setInt32(length); + return true; +} + +static bool GetMarks(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + auto& observers = GetShellContext(cx)->markObservers; + if (!observers) { + args.rval().setUndefined(); + return true; + } + + size_t length = observers->get().length(); + Rooted ret(cx, js::NewDenseEmptyArray(cx)); + if (!ret) { + return false; + } + + for (uint32_t i = 0; i < length; i++) { + const char* color; + JSObject* obj = observers->get()[i]; + if (!obj) { + color = "dead"; + } else if (obj->zone()->isGCPreparing()) { + color = "unmarked"; + } else { + gc::TenuredCell* cell = &obj->asTenured(); + if (cell->isMarkedGray()) { + color = "gray"; + } else if (cell->isMarkedBlack()) { + color = "black"; + } else { + color = "unmarked"; + } + } + JSString* s = JS_NewStringCopyZ(cx, color); + if (!s) { + return false; + } + if (!NewbornArrayPush(cx, ret, StringValue(s))) { + return false; + } + } + + args.rval().setObject(*ret); + return true; +} + +namespace js { +namespace shell { + +class ShellAutoEntryMonitor : JS::dbg::AutoEntryMonitor { + Vector log; + bool oom; + bool enteredWithoutExit; + + public: + explicit ShellAutoEntryMonitor(JSContext* cx) + : AutoEntryMonitor(cx), oom(false), enteredWithoutExit(false) {} + + ~ShellAutoEntryMonitor() { MOZ_ASSERT(!enteredWithoutExit); } + + void Entry(JSContext* cx, JSFunction* function, JS::HandleValue asyncStack, + const char* asyncCause) override { + MOZ_ASSERT(!enteredWithoutExit); + enteredWithoutExit = true; + + RootedString displayId(cx, JS_GetFunctionDisplayId(function)); + if (displayId) { + UniqueChars displayIdStr = JS_EncodeStringToUTF8(cx, displayId); + if (!displayIdStr) { + // We report OOM in buildResult. + cx->recoverFromOutOfMemory(); + oom = true; + return; + } + oom = !log.append(std::move(displayIdStr)); + return; + } + + oom = !log.append(DuplicateString("anonymous")); + } + + void Entry(JSContext* cx, JSScript* script, JS::HandleValue asyncStack, + const char* asyncCause) override { + MOZ_ASSERT(!enteredWithoutExit); + enteredWithoutExit = true; + + UniqueChars label(JS_smprintf("eval:%s", JS_GetScriptFilename(script))); + oom = !label || !log.append(std::move(label)); + } + + void Exit(JSContext* cx) override { + MOZ_ASSERT(enteredWithoutExit); + enteredWithoutExit = false; + } + + bool buildResult(JSContext* cx, MutableHandleValue resultValue) { + if (oom) { + JS_ReportOutOfMemory(cx); + return false; + } + + RootedObject result(cx, JS::NewArrayObject(cx, log.length())); + if (!result) { + return false; + } + + for (size_t i = 0; i < log.length(); i++) { + char* name = log[i].get(); + RootedString string(cx, AtomizeUTF8Chars(cx, name, strlen(name))); + if (!string) { + return false; + } + RootedValue value(cx, StringValue(string)); + if (!JS_SetElement(cx, result, i, value)) { + return false; + } + } + + resultValue.setObject(*result.get()); + return true; + } +}; + +} // namespace shell +} // namespace js + +static bool EntryPoints(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + RootedObject opts(cx, ToObject(cx, args[0])); + if (!opts) { + return false; + } + + // { function: f } --- Call f. + { + RootedValue fun(cx), dummy(cx); + + if (!JS_GetProperty(cx, opts, "function", &fun)) { + return false; + } + if (!fun.isUndefined()) { + js::shell::ShellAutoEntryMonitor sarep(cx); + if (!Call(cx, UndefinedHandleValue, fun, JS::HandleValueArray::empty(), + &dummy)) { + return false; + } + return sarep.buildResult(cx, args.rval()); + } + } + + // { object: o, property: p, value: v } --- Fetch o[p], or if + // v is present, assign o[p] = v. + { + RootedValue objectv(cx), propv(cx), valuev(cx); + + if (!JS_GetProperty(cx, opts, "object", &objectv) || + !JS_GetProperty(cx, opts, "property", &propv)) + return false; + if (!objectv.isUndefined() && !propv.isUndefined()) { + RootedObject object(cx, ToObject(cx, objectv)); + if (!object) { + return false; + } + + RootedString string(cx, ToString(cx, propv)); + if (!string) { + return false; + } + RootedId id(cx); + if (!JS_StringToId(cx, string, &id)) { + return false; + } + + if (!JS_GetProperty(cx, opts, "value", &valuev)) { + return false; + } + + js::shell::ShellAutoEntryMonitor sarep(cx); + + if (!valuev.isUndefined()) { + if (!JS_SetPropertyById(cx, object, id, valuev)) { + return false; + } + } else { + if (!JS_GetPropertyById(cx, object, id, &valuev)) { + return false; + } + } + + return sarep.buildResult(cx, args.rval()); + } + } + + // { ToString: v } --- Apply JS::ToString to v. + { + RootedValue v(cx); + + if (!JS_GetProperty(cx, opts, "ToString", &v)) { + return false; + } + if (!v.isUndefined()) { + js::shell::ShellAutoEntryMonitor sarep(cx); + if (!JS::ToString(cx, v)) { + return false; + } + return sarep.buildResult(cx, args.rval()); + } + } + + // { ToNumber: v } --- Apply JS::ToNumber to v. + { + RootedValue v(cx); + double dummy; + + if (!JS_GetProperty(cx, opts, "ToNumber", &v)) { + return false; + } + if (!v.isUndefined()) { + js::shell::ShellAutoEntryMonitor sarep(cx); + if (!JS::ToNumber(cx, v, &dummy)) { + return false; + } + return sarep.buildResult(cx, args.rval()); + } + } + + // { eval: code } --- Apply ToString and then Evaluate to code. + { + RootedValue code(cx), dummy(cx); + + if (!JS_GetProperty(cx, opts, "eval", &code)) { + return false; + } + if (!code.isUndefined()) { + RootedString codeString(cx, ToString(cx, code)); + if (!codeString) { + return false; + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, codeString)) { + return false; + } + JS::SourceText srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + CompileOptions options(cx); + options.setIntroductionType("entryPoint eval") + .setFileAndLine("entryPoint eval", 1); + + js::shell::ShellAutoEntryMonitor sarep(cx); + if (!JS::Evaluate(cx, options, srcBuf, &dummy)) { + return false; + } + return sarep.buildResult(cx, args.rval()); + } + } + + JS_ReportErrorASCII(cx, "bad 'params' object"); + return false; +} + +#ifndef __wasi__ +static bool WasmTextToBinary(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (!args.requireAtLeast(cx, "wasmTextToBinary", 1)) { + return false; + } + + if (!args[0].isString()) { + ReportUsageErrorASCII(cx, callee, "First argument must be a String"); + return false; + } + + size_t textLen = args[0].toString()->length(); + + AutoStableStringChars twoByteChars(cx); + if (!twoByteChars.initTwoByte(cx, args[0].toString())) { + return false; + } + + wasm::Bytes bytes; + UniqueChars error; + if (!wasm::TextToBinary(twoByteChars.twoByteChars(), textLen, &bytes, + &error)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_TEXT_FAIL, + error.get() ? error.get() : "out of memory"); + return false; + } + + RootedObject binary(cx, JS_NewUint8Array(cx, bytes.length())); + if (!binary) { + return false; + } + + memcpy(binary->as().dataPointerUnshared(), bytes.begin(), + bytes.length()); + + args.rval().setObject(*binary); + return true; +} + +# ifndef __AFL_HAVE_MANUAL_CONTROL +# define __AFL_LOOP(x) true +# endif + +static bool WasmLoop(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() < 1 || args.length() > 2) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + if (!args[0].isString()) { + ReportUsageErrorASCII(cx, callee, "First argument must be a String"); + return false; + } + + RootedObject importObj(cx); + if (!args.get(1).isUndefined()) { + if (!args.get(1).isObject()) { + ReportUsageErrorASCII(cx, callee, + "Second argument, if present, must be an Object"); + return false; + } + importObj = &args[1].toObject(); + } + + RootedString givenPath(cx, args[0].toString()); + RootedString filename(cx, ResolvePath(cx, givenPath, RootRelative)); + if (!filename) { + return false; + } + + while (__AFL_LOOP(1000)) { + Rooted ret(cx, FileAsTypedArray(cx, filename)); + if (!ret) { + return false; + } + + Rooted typedArray(cx, &ret->as()); + Rooted instanceObj(cx); + if (!wasm::Eval(cx, typedArray, importObj, &instanceObj)) { + // Clear any pending exceptions, we don't care about them + cx->clearPendingException(); + } + } + +# ifdef __AFL_HAVE_MANUAL_CONTROL // to silence unreachable code warning + return true; +# endif +} +#endif // __wasi__ + +static constexpr uint32_t DOM_OBJECT_SLOT = 0; +static constexpr uint32_t DOM_OBJECT_SLOT2 = 1; + +static const JSClass* GetDomClass(); + +static JSObject* GetDOMPrototype(JSContext* cx, JSObject* global); + +static const JSClass TransplantableDOMObjectClass = { + "TransplantableDOMObject", + JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(1)}; + +static const JSClass TransplantableDOMProxyObjectClass = + PROXY_CLASS_DEF("TransplantableDOMProxyObject", + JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(1)); + +class TransplantableDOMProxyHandler final : public ForwardingProxyHandler { + public: + static const TransplantableDOMProxyHandler singleton; + static const char family; + + constexpr TransplantableDOMProxyHandler() : ForwardingProxyHandler(&family) {} + + // These two proxy traps are called in |js::DeadProxyTargetValue|, which in + // turn is called when nuking proxies. Because this proxy can temporarily be + // without an object in its private slot, see |EnsureExpandoObject|, the + // default implementation inherited from ForwardingProxyHandler can't be used, + // since it tries to derive the callable/constructible value from the target. + bool isCallable(JSObject* obj) const override { return false; } + bool isConstructor(JSObject* obj) const override { return false; } + + // Simplified implementation of |DOMProxyHandler::GetAndClearExpandoObject|. + static JSObject* GetAndClearExpandoObject(JSObject* obj) { + const Value& v = GetProxyPrivate(obj); + if (v.isUndefined()) { + return nullptr; + } + + JSObject* expandoObject = &v.toObject(); + SetProxyPrivate(obj, UndefinedValue()); + return expandoObject; + } + + // Simplified implementation of |DOMProxyHandler::EnsureExpandoObject|. + static JSObject* EnsureExpandoObject(JSContext* cx, JS::HandleObject obj) { + const Value& v = GetProxyPrivate(obj); + if (v.isObject()) { + return &v.toObject(); + } + MOZ_ASSERT(v.isUndefined()); + + JSObject* expando = JS_NewObjectWithGivenProto(cx, nullptr, nullptr); + if (!expando) { + return nullptr; + } + SetProxyPrivate(obj, ObjectValue(*expando)); + return expando; + } +}; + +const TransplantableDOMProxyHandler TransplantableDOMProxyHandler::singleton; +const char TransplantableDOMProxyHandler::family = 0; + +enum TransplantObjectSlots { + TransplantSourceObject = 0, +}; + +static bool TransplantObject(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedFunction callee(cx, &args.callee().as()); + + if (args.length() != 1 || !args[0].isObject()) { + JS_ReportErrorASCII(cx, "transplant() must be called with an object"); + return false; + } + + // |newGlobal| needs to be a GlobalObject. + RootedObject newGlobal( + cx, js::CheckedUnwrapDynamic(&args[0].toObject(), cx, + /* stopAtWindowProxy = */ false)); + if (!newGlobal) { + ReportAccessDenied(cx); + return false; + } + if (!JS_IsGlobalObject(newGlobal)) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "\"global\" passed to transplant()", "not a global object"); + return false; + } + + const Value& reserved = + GetFunctionNativeReserved(callee, TransplantSourceObject); + RootedObject source(cx, CheckedUnwrapStatic(&reserved.toObject())); + if (!source) { + ReportAccessDenied(cx); + return false; + } + MOZ_ASSERT(source->getClass()->isDOMClass()); + + // The following steps aim to replicate the behavior of UpdateReflectorGlobal + // in dom/bindings/BindingUtils.cpp. In detail: + // 1. Check the recursion depth using checkConservative. + // 2. Enter the target compartment. + // 3. Clone the source object using JS_CloneObject. + // 4. Check if new wrappers can be created if source and target are in + // different compartments. + // 5. Copy all properties from source to a temporary holder object. + // 6. Actually transplant the object. + // 7. And finally copy the properties back to the source object. + // + // As an extension to the algorithm in UpdateReflectorGlobal, we also allow + // to transplant an object into the same compartment as the source object to + // cover all operations supported by JS_TransplantObject. + + AutoCheckRecursionLimit recursion(cx); + if (!recursion.checkConservative(cx)) { + return false; + } + + bool isProxy = IsProxy(source); + RootedObject expandoObject(cx); + if (isProxy) { + expandoObject = + TransplantableDOMProxyHandler::GetAndClearExpandoObject(source); + } + + JSAutoRealm ar(cx, newGlobal); + + RootedObject proto(cx); + if (JS::GetClass(source) == GetDomClass()) { + proto = GetDOMPrototype(cx, newGlobal); + } else { + proto = JS::GetRealmObjectPrototype(cx); + } + if (!proto) { + return false; + } + + RootedObject target(cx, JS_CloneObject(cx, source, proto)); + if (!target) { + return false; + } + + if (JS::GetCompartment(source) != JS::GetCompartment(target) && + !AllowNewWrapper(JS::GetCompartment(source), target)) { + JS_ReportErrorASCII(cx, "Cannot transplant into nuked compartment"); + return false; + } + + RootedObject copyFrom(cx, isProxy ? expandoObject : source); + RootedObject propertyHolder(cx, + JS_NewObjectWithGivenProto(cx, nullptr, nullptr)); + if (!propertyHolder) { + return false; + } + + if (!JS_CopyOwnPropertiesAndPrivateFields(cx, propertyHolder, copyFrom)) { + return false; + } + + JS::SetReservedSlot(target, DOM_OBJECT_SLOT, + JS::GetReservedSlot(source, DOM_OBJECT_SLOT)); + JS::SetReservedSlot(source, DOM_OBJECT_SLOT, JS::PrivateValue(nullptr)); + if (JS::GetClass(source) == GetDomClass()) { + JS::SetReservedSlot(target, DOM_OBJECT_SLOT2, + JS::GetReservedSlot(source, DOM_OBJECT_SLOT2)); + JS::SetReservedSlot(source, DOM_OBJECT_SLOT2, UndefinedValue()); + } + + source = JS_TransplantObject(cx, source, target); + if (!source) { + return false; + } + + RootedObject copyTo(cx); + if (isProxy) { + copyTo = TransplantableDOMProxyHandler::EnsureExpandoObject(cx, source); + if (!copyTo) { + return false; + } + } else { + copyTo = source; + } + if (!JS_CopyOwnPropertiesAndPrivateFields(cx, copyTo, propertyHolder)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool TransplantableObject(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() > 1) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + bool createProxy = false; + RootedObject source(cx); + if (args.length() == 1 && !args[0].isUndefined()) { + if (!args[0].isObject()) { + ReportUsageErrorASCII(cx, callee, "Argument must be an object"); + return false; + } + + RootedObject options(cx, &args[0].toObject()); + RootedValue value(cx); + + if (!JS_GetProperty(cx, options, "proxy", &value)) { + return false; + } + createProxy = JS::ToBoolean(value); + + if (!JS_GetProperty(cx, options, "object", &value)) { + return false; + } + if (!value.isUndefined()) { + if (!value.isObject()) { + ReportUsageErrorASCII(cx, callee, "'object' option must be an object"); + return false; + } + + source = &value.toObject(); + if (JS::GetClass(source) != GetDomClass()) { + ReportUsageErrorASCII(cx, callee, "Object not a FakeDOMObject"); + return false; + } + + // |source| must be a tenured object to be transplantable. + if (gc::IsInsideNursery(source)) { + JS_GC(cx); + + MOZ_ASSERT(!gc::IsInsideNursery(source), + "Live objects should be tenured after one GC, because " + "the nursery has only a single generation"); + } + } + } + + if (!source) { + if (!createProxy) { + source = NewBuiltinClassInstance(cx, &TransplantableDOMObjectClass, + TenuredObject); + if (!source) { + return false; + } + + JS::SetReservedSlot(source, DOM_OBJECT_SLOT, JS::PrivateValue(nullptr)); + } else { + JSObject* expando = JS_NewPlainObject(cx); + if (!expando) { + return false; + } + RootedValue expandoVal(cx, ObjectValue(*expando)); + + ProxyOptions options; + options.setClass(&TransplantableDOMProxyObjectClass); + options.setLazyProto(true); + + source = NewProxyObject(cx, &TransplantableDOMProxyHandler::singleton, + expandoVal, nullptr, options); + if (!source) { + return false; + } + + SetProxyReservedSlot(source, DOM_OBJECT_SLOT, JS::PrivateValue(nullptr)); + } + } + + jsid emptyId = NameToId(cx->names().empty); + RootedObject transplant( + cx, NewFunctionByIdWithReserved(cx, TransplantObject, 0, 0, emptyId)); + if (!transplant) { + return false; + } + + SetFunctionNativeReserved(transplant, TransplantSourceObject, + ObjectValue(*source)); + + RootedObject result(cx, JS_NewPlainObject(cx)); + if (!result) { + return false; + } + + RootedValue sourceVal(cx, ObjectValue(*source)); + RootedValue transplantVal(cx, ObjectValue(*transplant)); + if (!JS_DefineProperty(cx, result, "object", sourceVal, 0) || + !JS_DefineProperty(cx, result, "transplant", transplantVal, 0)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +#ifdef DEBUG +static bool DebugGetQueuedJobs(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JSObject* jobs = js::GetJobsInInternalJobQueue(cx); + if (!jobs) { + return false; + } + + args.rval().setObject(*jobs); + return true; +} +#endif + +#ifdef FUZZING_INTERFACES +extern "C" { +size_t gluesmith(uint8_t* data, size_t size, uint8_t* out, size_t maxsize); +} + +static bool GetWasmSmithModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 1) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is()) { + ReportUsageErrorASCII(cx, callee, "Argument must be ArrayBuffer."); + return false; + } + + ArrayBufferObject* arrayBuffer = &args[0].toObject().as(); + size_t length = arrayBuffer->byteLength(); + uint8_t* data = arrayBuffer->dataPointer(); + + const size_t maxModuleSize = 4096; + uint8_t tmp[maxModuleSize]; + + size_t outSize = gluesmith(data, length, tmp, maxModuleSize); + if (!outSize) { + JS_ReportErrorASCII(cx, "Generated module is too large."); + return false; + } + + JS::Rooted outArr(cx, JS_NewUint8ClampedArray(cx, outSize)); + if (!outArr) { + return false; + } + + { + JS::AutoCheckCannotGC nogc; + bool isShared; + uint8_t* data = JS_GetUint8ClampedArrayData(outArr, &isShared, nogc); + MOZ_RELEASE_ASSERT(!isShared); + memcpy(data, tmp, outSize); + } + + args.rval().setObject(*outArr); + return true; +} + +#endif + +static bool IsValidJSON(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (!args.get(0).isString()) { + ReportUsageErrorASCII(cx, callee, "First argument must be a String"); + return false; + } + + JS::Rooted input(cx, args[0].toString()->ensureLinear(cx)); + if (!input) { + return false; + } + + bool result; + if (input->hasLatin1Chars()) { + JS::AutoCheckCannotGC nogc; + result = JS::IsValidJSON(input->latin1Chars(nogc), input->length()); + } else { + JS::AutoCheckCannotGC nogc; + result = JS::IsValidJSON(input->twoByteChars(nogc), input->length()); + } + + args.rval().setBoolean(result); + return true; +} + +// clang-format off +static const JSFunctionSpecWithHelp shell_functions[] = { + JS_FN_HELP("options", Options, 0, 0, +"options([option ...])", +" Get or toggle JavaScript options."), + + JS_FN_HELP("load", Load, 1, 0, +"load(['foo.js' ...])", +" Load files named by string arguments. Filename is relative to the\n" +" current working directory."), + + JS_FN_HELP("loadRelativeToScript", LoadScriptRelativeToScript, 1, 0, +"loadRelativeToScript(['foo.js' ...])", +" Load files named by string arguments. Filename is relative to the\n" +" calling script."), + + JS_FN_HELP("evaluate", Evaluate, 2, 0, +"evaluate(code[, options])", +" Evaluate code as though it were the contents of a file.\n" +" options is an optional object that may have these properties:\n" +" isRunOnce: use the isRunOnce compiler option (default: false)\n" +" noScriptRval: use the no-script-rval compiler option (default: false)\n" +" fileName: filename for error messages and debug info\n" +" skipFileNameValidation: skip the filename-validation callback\n" +" lineNumber: starting line number for error messages and debug info\n" +" columnNumber: starting column number for error messages and debug info\n" +" global: global in which to execute the code\n" +" newContext: if true, create and use a new cx (default: false)\n" +" catchTermination: if true, catch termination (failure without\n" +" an exception value, as for slow scripts or out-of-memory)\n" +" and return 'terminated'\n" +" element: if present with value |v|, convert |v| to an object |o| and\n" +" mark the source as being attached to the DOM element |o|. If the\n" +" property is omitted or |v| is null, don't attribute the source to\n" +" any DOM element.\n" +" elementAttributeName: if present and not undefined, the name of\n" +" property of 'element' that holds this code. This is what\n" +" Debugger.Source.prototype.elementAttributeName returns.\n" +" sourceMapURL: if present with value |v|, convert |v| to a string, and\n" +" provide that as the code's source map URL. If omitted, attach no\n" +" source map URL to the code (although the code may provide one itself,\n" +" via a //#sourceMappingURL comment).\n" +" sourceIsLazy: if present and true, indicates that, after compilation, \n" +" script source should not be cached by the JS engine and should be \n" +" lazily loaded from the embedding as-needed.\n" +" forceFullParse: if present and true, disable syntax-parse.\n" +" loadBytecode: if true, and if the source is a CacheEntryObject,\n" +" the bytecode would be loaded and decoded from the cache entry instead\n" +" of being parsed, then it would be executed as usual.\n" +" saveIncrementalBytecode: if true, and if the source is a\n" +" CacheEntryObject, the bytecode would be incrementally encoded and\n" +" saved into the cache entry.\n" +" execute: if false, do not execute the script, but do parse and/or\n" +" transcode.\n" +" assertEqBytecode: if true, and if both loadBytecode and either\n" +" saveIncrementalBytecode is true, then the loaded\n" +" bytecode and the encoded bytecode are compared.\n" +" and an assertion is raised if they differ.\n" +" envChainObject: object to put on the scope chain, with its fields added\n" +" as var bindings, akin to how elements are added to the environment in\n" +" event handlers in Gecko.\n" +), + + JS_FN_HELP("run", Run, 1, 0, +"run('foo.js')", +" Run the file named by the first argument, returning the number of\n" +" of milliseconds spent compiling and executing it."), + + JS_FN_HELP("readline", ReadLine, 0, 0, +"readline()", +" Read a single line from stdin."), + + JS_FN_HELP("readlineBuf", ReadLineBuf, 1, 0, +"readlineBuf([ buf ])", +" Emulate readline() on the specified string. The first call with a string\n" +" argument sets the source buffer. Subsequent calls without an argument\n" +" then read from this buffer line by line.\n"), + + JS_FN_HELP("print", Print, 0, 0, +"print([exp ...])", +" Evaluate and print expressions to stdout."), + + JS_FN_HELP("printErr", PrintErr, 0, 0, +"printErr([exp ...])", +" Evaluate and print expressions to stderr."), + + JS_FN_HELP("putstr", PutStr, 0, 0, +"putstr([exp])", +" Evaluate and print expression without newline."), + + JS_FN_HELP("dateNow", Now, 0, 0, +"dateNow()", +" Return the current time with sub-ms precision."), + + JS_FN_HELP("help", Help, 0, 0, +"help([function or interface object or /pattern/])", +" Display usage and help messages."), + + JS_FN_HELP("quit", Quit, 0, 0, +"quit()", +" Quit the shell."), + + JS_FN_HELP("assertEq", AssertEq, 2, 0, +"assertEq(actual, expected[, msg])", +" Throw if the first two arguments are not the same (both +0 or both -0,\n" +" both NaN, or non-zero and ===)."), + + JS_FN_HELP("startTimingMutator", StartTimingMutator, 0, 0, +"startTimingMutator()", +" Start accounting time to mutator vs GC."), + + JS_FN_HELP("stopTimingMutator", StopTimingMutator, 0, 0, +"stopTimingMutator()", +" Stop accounting time to mutator vs GC and dump the results."), + + JS_FN_HELP("throwError", ThrowError, 0, 0, +"throwError()", +" Throw an error from JS_ReportError."), + + JS_FN_HELP("createErrorReport", CreateErrorReport, 1, 0, +"createErrorReport(value)", +" Create an JS::ErrorReportBuilder object from the given value and serialize\n" +" to an object."), + +#if defined(DEBUG) || defined(JS_JITSPEW) + JS_FN_HELP("disassemble", DisassembleToString, 1, 0, +"disassemble([fun/code])", +" Return the disassembly for the given function or code.\n" +" All disassembly functions take these options as leading string arguments:\n" +" \"-r\" (disassemble recursively)\n" +" \"-l\" (show line numbers)\n" +" \"-S\" (omit source notes)"), + + JS_FN_HELP("dis", Disassemble, 1, 0, +"dis([fun/code])", +" Disassemble functions into bytecodes."), + + JS_FN_HELP("disfile", DisassFile, 1, 0, +"disfile('foo.js')", +" Disassemble script file into bytecodes.\n"), + + JS_FN_HELP("dissrc", DisassWithSrc, 1, 0, +"dissrc([fun/code])", +" Disassemble functions with source lines."), + + JS_FN_HELP("notes", Notes, 1, 0, +"notes([fun])", +" Show source notes for functions."), + + JS_FN_HELP("stackDump", StackDump, 3, 0, +"stackDump(showArgs, showLocals, showThisProps)", +" Tries to print a lot of information about the current stack. \n" +" Similar to the DumpJSStack() function in the browser."), + +#endif + + JS_FN_HELP("getslx", GetSLX, 1, 0, +"getslx(obj)", +" Get script line extent."), + + JS_FN_HELP("evalcx", EvalInContext, 1, 0, +"evalcx(s[, o])", +" Evaluate s in optional sandbox object o.\n" +" if (s == '' && !o) return new o with eager standard classes\n" +" if (s == 'lazy' && !o) return new o with lazy standard classes"), + + JS_FN_HELP("evalInWorker", EvalInWorker, 1, 0, +"evalInWorker(str)", +" Evaluate 'str' in a separate thread with its own runtime.\n"), + + JS_FN_HELP("getSharedObject", GetSharedObject, 0, 0, +"getSharedObject()", +" Retrieve the shared object from the cross-worker mailbox.\n" +" The object retrieved may not be identical to the object that was\n" +" installed, but it references the same shared memory.\n" +" getSharedObject performs an ordering memory barrier.\n"), + + JS_FN_HELP("setSharedObject", SetSharedObject, 0, 0, +"setSharedObject(obj)", +" Install the shared object in the cross-worker mailbox. The object\n" +" may be null. setSharedObject performs an ordering memory barrier.\n"), + + JS_FN_HELP("getSharedArrayBuffer", GetSharedObject, 0, 0, +"getSharedArrayBuffer()", +" Obsolete alias for getSharedObject().\n"), + + JS_FN_HELP("setSharedArrayBuffer", SetSharedObject, 0, 0, +"setSharedArrayBuffer(obj)", +" Obsolete alias for setSharedObject(obj).\n"), + + JS_FN_HELP("shapeOf", ShapeOf, 1, 0, +"shapeOf(obj)", +" Get the shape of obj (an implementation detail)."), + +#ifdef DEBUG + JS_FN_HELP("arrayInfo", ArrayInfo, 1, 0, +"arrayInfo(a1, a2, ...)", +" Report statistics about arrays."), +#endif + + JS_FN_HELP("sleep", Sleep_fn, 1, 0, +"sleep(dt)", +" Sleep for dt seconds."), + + JS_FN_HELP("parseModule", ParseModule, 1, 0, +"parseModule(code)", +" Parses source text as a module and returns a ModuleObject wrapper object."), + + JS_FN_HELP("instantiateModuleStencil", InstantiateModuleStencil, 1, 0, +"instantiateModuleStencil(stencil, [options])", +" Instantiates the given stencil as module, and return the module object."), + + JS_FN_HELP("instantiateModuleStencilXDR", InstantiateModuleStencilXDR, 1, 0, +"instantiateModuleStencilXDR(stencil, [options])", +" Reads the given stencil XDR object, instantiates the stencil as module, and" +" return the module object."), + + JS_FN_HELP("registerModule", RegisterModule, 2, 0, +"registerModule(specifier, module)", +" Register a module with the module loader, so that subsequent import from\n" +" |specifier| will resolve to |module|. Returns |module|."), + + JS_FN_HELP("clearModules", ClearModules, 0, 0, +"clearModules()", +" Clear knowledge of all loaded modules."), + + JS_FN_HELP("moduleLink", ModuleLink, 1, 0, +"moduleLink(moduleOjbect)", +" Link a module graph, performing the spec's Link method."), + + JS_FN_HELP("moduleEvaluate", ModuleEvaluate, 1, 0, +"moduleEvaluate(moduleOjbect)", +" Evaluate a module graph, performing the spec's Evaluate method."), + + JS_FN_HELP("getModuleEnvironmentNames", GetModuleEnvironmentNames, 1, 0, +"getModuleEnvironmentNames(module)", +" Get the list of a module environment's bound names for a specified module.\n"), + + JS_FN_HELP("getModuleEnvironmentValue", GetModuleEnvironmentValue, 2, 0, +"getModuleEnvironmentValue(module, name)", +" Get the value of a bound name in a module environment.\n"), + + JS_FN_HELP("dumpStencil", DumpStencil, 1, 0, +"dumpStencil(code, [options])", +" Parses a string and returns string that represents stencil.\n" +" If present, |options| may have properties saying how the code should be\n" +" compiled:\n" +" module: if present and true, compile the source as module.\n" +" smoosh: if present and true, use SmooshMonkey.\n" +" CompileOptions-related properties of evaluate function's option can also\n" +" be used."), + + JS_FN_HELP("parse", Parse, 1, 0, +"parse(code, [options])", +" Parses a string, potentially throwing. If present, |options| may\n" +" have properties saying how the code should be compiled:\n" +" module: if present and true, compile the source as module.\n" +" smoosh: if present and true, use SmooshMonkey.\n" +" CompileOptions-related properties of evaluate function's option can also\n" +" be used. except forceFullParse. This function always use full parse."), + + JS_FN_HELP("syntaxParse", SyntaxParse, 1, 0, +"syntaxParse(code)", +" Check the syntax of a string, returning success value"), + + JS_FN_HELP("offThreadCompileModuleToStencil", OffThreadCompileModuleToStencil, 1, 0, +"offThreadCompileModuleToStencil(code)", +" Compile |code| on a helper thread, returning a job ID. To wait for the\n" +" compilation to finish and and get the module stencil object call\n" +" |finishOffThreadStencil| passing the job ID."), + + JS_FN_HELP("offThreadDecodeStencil", OffThreadDecodeStencil, 1, 0, +"offThreadDecodeStencil(cacheEntry[, options])", +" Decode |code| on a helper thread, returning a job ID. To wait for the\n" +" decoding to finish and run the code, call |finishOffThreadStencil| passing\n" +" the job ID. If present, |options| may have properties saying how the code\n" +" should be compiled (see also offThreadCompileToStencil)."), + + JS_FN_HELP("offThreadCompileToStencil", OffThreadCompileToStencil, 1, 0, +"offThreadCompileToStencil(code[, options])", +" Compile |code| on a helper thread, returning a job ID. To wait for the\n" +" compilation to finish and get the stencil object, call\n" +" |finishOffThreadStencil| passing the job ID. If present, \n" +" |options| may have properties saying how the code should be compiled:\n" +" noScriptRval: use the no-script-rval compiler option (default: false)\n" +" fileName: filename for error messages and debug info\n" +" lineNumber: starting line number for error messages and debug info\n" +" columnNumber: starting column number for error messages and debug info\n" +" element: if present with value |v|, convert |v| to an object |o| and\n" +" mark the source as being attached to the DOM element |o|. If the\n" +" property is omitted or |v| is null, don't attribute the source to\n" +" any DOM element.\n" +" elementAttributeName: if present and not undefined, the name of\n" +" property of 'element' that holds this code. This is what\n" +" Debugger.Source.prototype.elementAttributeName returns."), + + JS_FN_HELP("finishOffThreadStencil", FinishOffThreadStencil, 0, 0, +"finishOffThreadStencil([jobID])", +" Wait for an off-thread compilation or decode job to complete. The job ID\n" +" can be ommitted if there is only one job pending. If an error occurred,\n" +" throw the appropriate exception; otherwise, return the stencil object," +" that can be passed to |evalStencil|."), + + JS_FN_HELP("timeout", Timeout, 1, 0, +"timeout([seconds], [func])", +" Get/Set the limit in seconds for the execution time for the current context.\n" +" When the timeout expires the current interrupt callback is invoked.\n" +" The timeout is used just once. If the callback returns a falsy value, the\n" +" script is aborted. A negative value for seconds (this is the default) cancels\n" +" any pending timeout.\n" +" If a second argument is provided, it is installed as the interrupt handler,\n" +" exactly as if by |setInterruptCallback|.\n"), + + JS_FN_HELP("interruptIf", InterruptIf, 1, 0, +"interruptIf(cond)", +" Requests interrupt callback if cond is true. If a callback function is set via\n" +" |timeout| or |setInterruptCallback|, it will be called. No-op otherwise."), + + JS_FN_HELP("invokeInterruptCallback", InvokeInterruptCallbackWrapper, 0, 0, +"invokeInterruptCallback(fun)", +" Forcefully set the interrupt flag and invoke the interrupt handler. If a\n" +" callback function is set via |timeout| or |setInterruptCallback|, it will\n" +" be called. Before returning, fun is called with the return value of the\n" +" interrupt handler."), + + JS_FN_HELP("setInterruptCallback", SetInterruptCallback, 1, 0, +"setInterruptCallback(func)", +" Sets func as the interrupt callback function.\n" +" Calling this function will replace any callback set by |timeout|.\n" +" If the callback returns a falsy value, the script is aborted.\n"), + + JS_FN_HELP("setJitCompilerOption", SetJitCompilerOption, 2, 0, +"setJitCompilerOption(