diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /js/src/shell | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/shell')
-rw-r--r-- | js/src/shell/Makefile.in | 16 | ||||
-rw-r--r-- | js/src/shell/ModuleLoader.cpp | 672 | ||||
-rw-r--r-- | js/src/shell/ModuleLoader.h | 94 | ||||
-rw-r--r-- | js/src/shell/OSObject.cpp | 1309 | ||||
-rw-r--r-- | js/src/shell/OSObject.h | 99 | ||||
-rw-r--r-- | js/src/shell/ShellModuleObjectWrapper.cpp | 469 | ||||
-rw-r--r-- | js/src/shell/ShellModuleObjectWrapper.h | 36 | ||||
-rw-r--r-- | js/src/shell/StringUtils.h | 146 | ||||
-rw-r--r-- | js/src/shell/WasmTesting.cpp | 64 | ||||
-rw-r--r-- | js/src/shell/WasmTesting.h | 37 | ||||
-rw-r--r-- | js/src/shell/fuzz-flags.txt | 97 | ||||
-rw-r--r-- | js/src/shell/js-gdb.py | 21 | ||||
-rw-r--r-- | js/src/shell/js.cpp | 12594 | ||||
-rw-r--r-- | js/src/shell/jsoptparse.cpp | 639 | ||||
-rw-r--r-- | js/src/shell/jsoptparse.h | 336 | ||||
-rw-r--r-- | js/src/shell/jsrtfuzzing/jsrtfuzzing-example.js | 42 | ||||
-rw-r--r-- | js/src/shell/jsrtfuzzing/jsrtfuzzing.cpp | 139 | ||||
-rw-r--r-- | js/src/shell/jsrtfuzzing/jsrtfuzzing.h | 28 | ||||
-rw-r--r-- | js/src/shell/jsshell.cpp | 128 | ||||
-rw-r--r-- | js/src/shell/jsshell.h | 263 | ||||
-rw-r--r-- | js/src/shell/moz.build | 59 |
21 files changed, 17288 insertions, 0 deletions
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<JSLinearString*> path) { + return StringStartsWith(path, JavaScriptScheme); +} + +static JSString* ExtractJavaScriptURLSource(JSContext* cx, + Handle<JSLinearString*> 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<ModuleObject*> module) { + Rooted<JSLinearString*> 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<GlobalObject*> 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<JSLinearString*> 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<JSLinearString*> path(cx); + if (!privateValue.isUndefined()) { + if (!getScriptPath(cx, privateValue, &path)) { + return false; + } + } + + if (!path) { + path = NewStringCopyZ<CanGC>(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<JS::Value> referencingPrivate, + JS::Handle<JSString*> specifier, + JS::MutableHandle<JSString*> urlOut) { + Rooted<JSLinearString*> 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<JSLinearString*> 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<ModuleRequestObject>(); + if (moduleRequest->specifier()->length() == 0) { + JS_ReportErrorASCII(cx, "Invalid module specifier"); + return nullptr; + } + + Rooted<JSLinearString*> 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<JSLinearString*> 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<JSLinearString*> 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<JSLinearString*> linear(cx, JS_EnsureLinearString(cx, result)); + if (!linear) { + return nullptr; + } + return normalizePath(cx, linear); +} + +JSObject* ModuleLoader::loadAndParse(JSContext* cx, HandleString pathArg) { + Rooted<JSLinearString*> 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<char16_t> 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<GlobalObject*> 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<JSLinearString*> 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<JSLinearString*> pathArg) { + Rooted<JSLinearString*> 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<JSLinearString*> 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<GCVector<JSLinearString*>> 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<JSLinearString*> 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<JSLinearString*> 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<JSLinearString*> 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<ModuleObject*> 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<JS::Value> referencingPrivate, + JS::Handle<JSString*> specifier, + JS::MutableHandle<JSString*> 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<JSLinearString*> pathOut); + JSLinearString* normalizePath(JSContext* cx, Handle<JSLinearString*> path); + JSObject* getOrCreateModuleRegistry(JSContext* cx); + JSString* fetchSource(JSContext* cx, Handle<JSLinearString*> 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 <errno.h> +#include <stdlib.h> +#ifdef XP_WIN +# include <direct.h> +# include <process.h> +# include <string.h> +# include <wchar.h> +# include <windows.h> +#elif __wasi__ +# include <dirent.h> +# include <sys/types.h> +# include <unistd.h> +#else +# include <dirent.h> +# include <sys/types.h> +# include <sys/wait.h> +# include <unistd.h> +#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 <libgen.h> +#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<JSLinearString*> 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<char>(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<JSObject*> obj(cx, JS_NewUint8Array(cx, len)); + if (!obj) { + return nullptr; + } + + js::TypedArrayObject& ta = obj->as<js::TypedArrayObject>(); + 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<char*>(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<JSString*> givenPath(cx, args[0].toString()); + JS::Rooted<JSString*> 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<char> 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<TypedArrayObject>()) { + 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<TypedArrayObject>(); + + 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_<RCFile>(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<FileObject>(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<FileObject>(); + 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<RCFile*>( + 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<FileObject*> fileObj(cx, + args[0].toObject().maybeUnwrapIf<FileObject>()); + 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<FileObject*> fileObj(cx); + if (args.get(0).isObject()) { + fileObj = args[0].toObject().maybeUnwrapIf<FileObject>(); + } + + 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<JSLinearString*> 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<JSLinearString*> 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<JSString*> 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<JSString*> 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 <stdio.h> + +#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<CLASS>(); \ + } \ + /* 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<JS::Value> v) { \ + return v.isObject() && v.toObject().is<Shell##CLASS##Wrapper>(); \ + } + +#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<CLASS*> obj); \ + CLASS* get(); \ + }; \ + DEFINE_CLASS_IMPL(CLASS) + +#define DEFINE_NATIVE_CLASS_IMPL(CLASS) \ + CLASS* Shell##CLASS##Wrapper::get() { \ + return static_cast<CLASS*>(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<JS::Value> v) { \ + return v.isObject() && v.toObject().is<Shell##CLASS##Wrapper>(); \ + } + +#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<JSObject*> 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<JS::Value> from, + JS::MutableHandle<JS::Value> to) { + to.set(from); + return true; +} + +template <class T> +bool SingleFilter(JSContext* cx, JS::Handle<JS::Value> from, + JS::MutableHandle<JS::Value> to) { + using TargetT = typename T::Target; + + if (!from.isObject() || !from.toObject().is<TargetT>()) { + to.set(from); + return true; + } + + JS::Rooted<TargetT*> obj(cx, &from.toObject().as<TargetT>()); + JS::Rooted<T*> filtered(cx, T::create(cx, obj)); + if (!filtered) { + return false; + } + to.setObject(*filtered); + return true; +} + +template <class T> +bool ArrayFilter(JSContext* cx, JS::Handle<JS::Value> from, + JS::MutableHandle<JS::Value> to) { + using TargetT = typename T::Target; + + if (!from.isObject() || !from.toObject().is<ArrayObject>()) { + to.set(from); + return true; + } + + JS::Rooted<ArrayObject*> fromArray(cx, &from.toObject().as<ArrayObject>()); + uint32_t length = fromArray->length(); + JS::Rooted<ArrayObject*> toArray(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!toArray) { + return false; + } + + toArray->ensureDenseInitializedLength(0, length); + + for (uint32_t i = 0; i < length; i++) { + JS::Rooted<JS::Value> item(cx, fromArray->getDenseElement(i)); + JS::Rooted<TargetT*> req(cx, &item.toObject().as<TargetT>()); + JS::Rooted<T*> filtered(cx, T::create(cx, req)); + if (!filtered) { + return false; + } + toArray->initDenseElement(i, ObjectValue(*filtered)); + } + to.setObject(*toArray); + return true; +} + +template <class T> +bool ListToArrayFilter(JSContext* cx, JS::Handle<JS::Value> from, + JS::MutableHandle<JS::Value> to) { + using TargetT = typename T::Target; + + if (!from.isObject() || !from.toObject().is<ListObject>()) { + to.set(from); + return true; + } + + JS::Rooted<ListObject*> fromList(cx, &from.toObject().as<ListObject>()); + uint32_t length = fromList->length(); + JS::Rooted<ArrayObject*> toArray(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!toArray) { + return false; + } + + toArray->ensureDenseInitializedLength(0, length); + + for (uint32_t i = 0; i < length; i++) { + JS::Rooted<JS::Value> item(cx, fromList->get(i)); + JS::Rooted<TargetT*> req(cx, &item.toObject().as<TargetT>()); + JS::Rooted<T*> 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<uint32_t> 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 <class T, typename RawGetterT, typename FilterT> +bool ShellModuleWrapperGetter(JSContext* cx, const JS::CallArgs& args, + RawGetterT rawGetter, FilterT filter) { + JS::Rooted<T*> wrapper(cx, &args.thisv().toObject().as<T>()); + JS::Rooted<JS::Value> raw(cx, rawGetter(wrapper->get())); + + JS::Rooted<JS::Value> 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<Shell##CLASS##Wrapper>( \ + 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<IsShell##CLASS##Wrapper, \ + Shell##CLASS##Wrapper_##PROP##Getter_impl>( \ + cx, args); \ + } + +template <class T> +bool SpanToArrayFilter(JSContext* cx, JS::Handle<JSObject*> owner, + Span<const typename T::Target> from, + JS::MutableHandle<JS::Value> to) { + size_t length = from.Length(); + JS::Rooted<ArrayObject*> 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<typename T::Target*>(&from[i]); + JS::Rooted<T*> filtered(cx, T::create(cx, owner, element)); + if (!filtered) { + return false; + } + toArray->initDenseElement(i, ObjectValue(*filtered)); + } + + to.setObject(*toArray); + return true; +} + +template <class T, typename RawGetterT, typename FilterT> +bool ShellModuleNativeWrapperGetter(JSContext* cx, const JS::CallArgs& args, + RawGetterT rawGetter, FilterT filter) { + JS::Rooted<T*> wrapper(cx, &args.thisv().toObject().as<T>()); + JS::Rooted<typename T::Target*> owner(cx, wrapper->get()); + + JS::Rooted<JS::Value> 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<Shell##CLASS##Wrapper>( \ + 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<IsShell##CLASS##Wrapper, \ + Shell##CLASS##Wrapper_##PROP##Getter_impl>( \ + 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<ShellModuleRequestObjectWrapper>) +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<ShellModuleRequestObjectWrapper>) +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<ShellModuleRequestObjectWrapper>) +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<ShellRequestedModuleWrapper>) +DEFINE_NATIVE_GETTER_FUNCTIONS(ModuleObject, importEntries, + SpanToArrayFilter<ShellImportEntryWrapper>) +DEFINE_NATIVE_GETTER_FUNCTIONS(ModuleObject, localExportEntries, + SpanToArrayFilter<ShellExportEntryWrapper>) +DEFINE_NATIVE_GETTER_FUNCTIONS(ModuleObject, indirectExportEntries, + SpanToArrayFilter<ShellExportEntryWrapper>) +DEFINE_NATIVE_GETTER_FUNCTIONS(ModuleObject, starExportEntries, + SpanToArrayFilter<ShellExportEntryWrapper>) +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<ShellModuleObjectWrapper>) +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<CLASS*> target) { \ + JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &class_)); \ + if (!obj) { \ + return nullptr; \ + } \ + if (!DefinePropertiesAndFunctions(cx, obj, ACCESSORS, FUNCTIONS)) { \ + return nullptr; \ + } \ + auto* wrapper = &obj->as<Shell##CLASS##Wrapper>(); \ + 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<JSObject*> owner, CLASS* target) { \ + JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &class_)); \ + if (!obj) { \ + return nullptr; \ + } \ + if (!DefinePropertiesAndFunctions(cx, obj, ACCESSORS, FUNCTIONS)) { \ + return nullptr; \ + } \ + auto* wrapper = &obj->as<Shell##CLASS##Wrapper>(); \ + 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<ModuleObject*> 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 <size_t NullTerminatedLength> +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 <size_t NullTerminatedLength> +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<JSLinearString*> 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<JSLinearString*> 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<JSLinearString*> 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<char16_t> 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<GCVector<JSLinearString*>> strings, + Handle<JSLinearString*> 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 <inttypes.h> +#include <stdbool.h> +#include <stddef.h> + +#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 <algorithm> +#include <chrono> +#ifdef XP_WIN +# include <direct.h> +# include <process.h> +#endif +#include <errno.h> +#include <fcntl.h> +#if defined(XP_WIN) +# include <io.h> /* for isatty() */ +#endif +#include <locale.h> +#if defined(MALLOC_H) +# include MALLOC_H /* for malloc_usable_size, malloc_size, _msize */ +#endif +#include <ctime> +#include <math.h> +#ifndef __wasi__ +# include <signal.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <utility> +#ifdef XP_UNIX +# ifndef __wasi__ +# include <sys/mman.h> +# include <sys/wait.h> +# endif +# include <sys/stat.h> +# include <unistd.h> +#endif +#ifdef XP_LINUX +# include <sys/prctl.h> +#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<int> 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<int> 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<JS::UniqueTwoByteChars, JS::TranscodeBuffer>; + + OffThreadJob(ShellContext* sc, Source&& source); + ~OffThreadJob(); + + void cancel(); + void markDone(JS::OffThreadToken* newToken); + JS::OffThreadToken* waitUntilDone(JSContext* cx); + + char16_t* sourceChars() { return source.as<UniqueTwoByteChars>().get(); } + JS::TranscodeBuffer& xdrBuffer() { return source.as<JS::TranscodeBuffer>(); } + + 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<OffThreadJob> job(cx->new_<OffThreadJob>(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<int32_t> 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<ArrayObject*> blackRoot; + GCPtr<ArrayObject*> 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<GlobalObject>()); + + 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<ShellPrincipals*>(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<const ShellPrincipals*>(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<ShellContext*>(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<ShellCompartmentPrivate*>( + JS_GetCompartmentPrivate(comp.get())); + if (!priv) { + continue; + } + + GCPtr<ArrayObject*>& 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<Value&>(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<char[]> 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<char[]> 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<char[]> buffer(static_cast<char*>(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<char*>(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<AutoReportException> 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<const char*>(buffer.begin()), + buffer.length()), + &length, js::MallocArena) + .get()); + if (!chars) { + return false; + } + + JS::SourceText<char16_t> 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<ShellContext*>(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<ShellContext::FunctionVector> 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<int32_t>(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<int>(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<Mutex> { + using Base = LockGuard<Mutex>; + + public: + AutoLockTelemetry() : Base(*telemetryLock) { MOZ_ASSERT(telemetryLock); } +}; + +using TelemetryData = uint32_t; +using TelemetryVec = Vector<TelemetryData, 0, SystemAllocPolicy>; +static mozilla::Array<TelemetryVec, size_t(JSMetric::Count)> telemetryResults; +static void AccumulateTelemetryDataCallback(JSMetric id, uint32_t sample) { + AutoLockTelemetry alt; + // We ignore OOMs while writting teleemtry data. + if (telemetryResults[static_cast<int>(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<SavedFrame*> stack(cx, nullptr); + bool isExplicit; + + RootedValue v(cx); + + if (!JS_GetProperty(cx, options, "stack", &v)) { + return false; + } + if (!v.isObject() || !v.toObject().is<SavedFrame>()) { + JS_ReportErrorASCII(cx, + "The 'stack' property must be a SavedFrame object."); + return false; + } + stack = &v.toObject().as<SavedFrame>(); + + 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<Utf8Unit> 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<char, 32> CharBuffer; + RootedObject globalLexical(cx, &cx->global()->lexicalEnvironment()); + CharBuffer buffer(cx); + do { + ScheduleWatchdog(cx, -1); + sc->serviceInterrupt = false; + errno = 0; + + mozilla::UniqueFreePtr<char[]> 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<JSString*> 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<JSFunction>() || !onReject || + !onReject->is<JSFunction>()) { + 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<JSLinearString*> 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<CacheOptions> { + using mozilla::EnumSet<CacheOptions>::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<ArrayBufferObject>()) { + JS_ReportErrorASCII( + cx, + "CacheEntry_getBytecode: Unexpected type of bytecode reserved slot."); + return nullptr; + } + + ArrayBufferObject* arrayBuffer = &v.toObject().as<ArrayBufferObject>(); + *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<ArrayBufferObject*> 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<CompileOptions> 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<JS::Stencil> 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<char16_t> 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<char>(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<char16_t> 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<char*>(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<char*>(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 "<<error converting value to string>>"; +} + +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<JSFunction>()) { + 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<ShellModuleObjectWrapper>()) { + script = value.toObject() + .as<ShellModuleObjectWrapper>() + .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<JSString*> 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<BaseScript>(); !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<ShellModuleObjectWrapper>()) { + script = + value.toObject().as<ShellModuleObjectWrapper>().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<JS::Value> 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<JSLinearString*> 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<uint32_t>(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<char*>(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<Utf8Unit> 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<const char16_t> 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<char16_t> 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<ProfilingStack>(); + 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<ShellCompartmentPrivate*>( + 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<WorkerInput> input) { + MOZ_ASSERT(input->parentRuntime); + + JSContext* cx = JS_NewContext(8L * 1024L * 1024L, input->parentRuntime); + if (!cx) { + return; + } + + ShellContext* sc = js_new<ShellContext>(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<ModuleLoader>(); + if (!sc->moduleLoader || !sc->moduleLoader->init(cx, moduleLoadPath)) { + return; + } + + JS::CompileOptions options(cx); + options.setFileAndLine("<string>", 1) + .setIsRunOnce(true) + .setEagerDelazificationStrategy(defaultDelazificationMode); + + AutoReportException are(cx); + JS::SourceText<char16_t> 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<UniquePtr<js::Thread>, 0, SystemAllocPolicy> workerThreads; + +class MOZ_RAII AutoLockWorkerThreads : public LockGuard<Mutex> { + using Base = LockGuard<Mutex>; + + 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<Mutex>(mutexid::ShellWorkerThreads); + if (!workerThreadsLock) { + ReportOutOfMemory(cx); + return false; + } + } + + JSLinearString* str = &args[0].toString()->asLinear(); + + UniqueTwoByteChars chars(js_pod_malloc<char16_t>(str->length())); + if (!chars) { + ReportOutOfMemory(cx); + return false; + } + + CopyChars(chars.get(), *str); + + auto input = js::MakeUnique<WorkerInput>(JS_GetParentRuntime(cx), + std::move(chars), str->length()); + if (!input) { + ReportOutOfMemory(cx); + return false; + } + + UniquePtr<Thread> thread; + { + AutoEnterOOMUnsafeRegion oomUnsafe; + thread = js::MakeUnique<Thread>( + 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<Mutex> 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> thread; + + { + LockGuard<Mutex> 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<Mutex> 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<Mutex> 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<Mutex> guard(sc->watchdogLock); + sc->watchdogTimeout = Nothing(); + return true; + } + +#ifdef __wasi__ + return false; +#endif + + auto interval = TimeDuration::FromSeconds(t); + auto timeout = TimeStamp::Now() + interval; + LockGuard<Mutex> 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> 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<JSFunction>()) { + 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<JSFunction>()) { + 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<RegExpObject>())) { + 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<size_t>(&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<ShellCompartmentPrivate*>(JS_GetCompartmentPrivate(comp)); + if (!priv) { + priv = cx->new_<ShellCompartmentPrivate>(); + 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("<string>", 1); + } + options.setModule(); + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, scriptContents)) { + return false; + } + + JS::SourceText<char16_t> 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<ShellModuleObjectWrapper*> wrapper( + cx, ShellModuleObjectWrapper::create(cx, module.as<ModuleObject>())); + 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<JS::TranscodeBuffer*>(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<XDRBufferObject>(cx, nullptr); + if (!bufObj) { + return nullptr; + } + + auto heapBuf = cx->make_unique<JS::TranscodeBuffer>(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<XDRBufferObject>(); + 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::StencilObject>()) { + JS_ReportErrorASCII(cx, + "instantiateModuleStencil: Stencil object expected"); + return false; + } + Rooted<js::StencilObject*> stencilObj( + cx, &args[0].toObject().as<js::StencilObject>()); + + 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<frontend::CompilationInput> input(cx, + frontend::CompilationInput(options)); + if (!input.get().initForModule(&fc)) { + return false; + } + + /* Instantiate the stencil. */ + Rooted<frontend::CompilationGCOutput> output(cx); + if (!frontend::CompilationStencil::instantiateStencils( + cx, input.get(), *stencilObj->stencil(), output.get())) { + return false; + } + + Rooted<ModuleObject*> modObject(cx, output.get().module); + Rooted<ShellModuleObjectWrapper*> 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<StencilXDRBufferObject>()) { + JS_ReportErrorASCII( + cx, "instantiateModuleStencilXDR: stencil XDR object expected"); + return false; + } + Rooted<StencilXDRBufferObject*> xdrObj( + cx, &args[0].toObject().as<StencilXDRBufferObject>()); + 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<frontend::CompilationInput> 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<frontend::CompilationGCOutput> output(cx); + if (!frontend::CompilationStencil::instantiateStencils( + cx, input.get(), stencil, output.get())) { + return false; + } + + Rooted<ModuleObject*> modObject(cx, output.get().module); + Rooted<ShellModuleObjectWrapper*> 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<ShellModuleObjectWrapper>()) { + const char* typeName = InformalValueTypeName(args[1]); + JS_ReportErrorASCII(cx, "expected module, got %s", typeName); + return false; + } + + ShellContext* sc = GetShellContext(cx); + Rooted<ModuleObject*> module( + cx, args[1].toObject().as<ShellModuleObjectWrapper>().get()); + + Rooted<JSAtom*> 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<ShellModuleObjectWrapper*> 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<ShellModuleObjectWrapper>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_ARGS, + "moduleLink"); + return false; + } + + AutoRealm ar(cx, object); + + Rooted<ModuleObject*> module(cx, + object->as<ShellModuleObjectWrapper>().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<ShellModuleObjectWrapper>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_ARGS, + "moduleEvaluate"); + return false; + } + + { + AutoRealm ar(cx, object); + + Rooted<ModuleObject*> module(cx, + object->as<ShellModuleObjectWrapper>().get()); + if (!js::ModuleEvaluate(cx, module, args.rval())) { + return false; + } + } + + return JS_WrapValue(cx, args.rval()); +} + +static ModuleEnvironmentObject* GetModuleInitialEnvironment( + JSContext* cx, Handle<ModuleObject*> module) { + // Use the initial environment so that tests can check bindings exists + // before they have been instantiated. + Rooted<ModuleEnvironmentObject*> 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<ShellModuleObjectWrapper>()) { + JS_ReportErrorASCII(cx, + "First argument should be a ShellModuleObjectWrapper"); + return false; + } + + Rooted<ModuleObject*> module( + cx, args[0].toObject().as<ShellModuleObjectWrapper>().get()); + if (module->hadEvaluationError()) { + JS_ReportErrorASCII(cx, "Module environment unavailable"); + return false; + } + + Rooted<ModuleEnvironmentObject*> env(cx, + GetModuleInitialEnvironment(cx, module)); + Rooted<IdVector> 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<ArrayObject*> 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<ShellModuleObjectWrapper>()) { + 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<ModuleObject*> module( + cx, args[0].toObject().as<ShellModuleObjectWrapper>().get()); + if (module->hadEvaluationError()) { + JS_ReportErrorASCII(cx, "Module environment unavailable"); + return false; + } + + Rooted<ModuleEnvironmentObject*> 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 <typename Unit> +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<FullParseHandler, Unit> 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 <typename Unit> +[[nodiscard]] static bool DumpStencil(JSContext* cx, + const JS::ReadOnlyCompileOptions& options, + const Unit* units, size_t length, + js::frontend::ParseGoal goal) { + Rooted<frontend::CompilationInput> input(cx, + frontend::CompilationInput(options)); + + JS::SourceText<Unit> srcBuf; + if (!srcBuf.init(cx, units, length, JS::SourceOwnership::Borrowed)) { + return false; + } + + AutoReportFrontendContext fc(cx); + js::frontend::NoScopeBindingCache scopeCache; + UniquePtr<frontend::ExtensibleCompilationStencil> 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("<string>", 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<JSLinearString*> 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<const char*>(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<const mozilla::Utf8Unit*>(latin1); + JS::SourceText<Utf8Unit> srcBuf; + if (!srcBuf.init(cx, utf8, length, JS::SourceOwnership::Borrowed)) { + return false; + } + + AutoReportFrontendContext fc(cx); + Rooted<frontend::CompilationInput> input( + cx, frontend::CompilationInput(options)); + UniquePtr<frontend::ExtensibleCompilationStencil> 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<const mozilla::Utf8Unit*>(latin1); + if (!DumpStencil<mozilla::Utf8Unit>(cx, options, utf8, length, goal)) { + return false; + } + } else { + MOZ_ASSERT(stableChars.isTwoByte()); + const char16_t* chars = stableChars.twoByteRange().begin().get(); + if (!DumpStencil<char16_t>(cx, options, chars, length, goal)) { + return false; + } + } + + args.rval().setUndefined(); + return true; + } + + AutoReportFrontendContext fc(cx); + Rooted<frontend::CompilationInput> 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<const mozilla::Utf8Unit*>(latin1); + if (!DumpAST<mozilla::Utf8Unit>(cx, options, utf8, length, compilationState, + goal)) { + return false; + } + } else { + MOZ_ASSERT(stableChars.isTwoByte()); + const char16_t* chars = stableChars.twoByteRange().begin().get(); + if (!DumpAST<char16_t>(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("<string>", 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<frontend::CompilationInput> 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<frontend::SyntaxParseHandler, char16_t> 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<OffThreadJob*>(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("<string>", 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<char16_t*>(chars)); + } else { + ownedChars.reset(cx->pod_malloc<char16_t>(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<char16_t> 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<JS::Stencil> 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("<string>", 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<char16_t*>(chars)); + } else { + ownedChars.reset(cx->pod_malloc<char16_t>(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<char16_t> 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("<string>", 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<char*> 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<char>(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<const char*, 5, js::SystemAllocPolicy> 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<wasm::ShareableBytes>(); + 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<uint8_t*> 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<JSFunction>()) { + args.rval().setUndefined(); + return true; + } + RootedFunction fun(cx, &args[0].toObject().as<JSFunction>()); + 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_<ShellPrincipals>(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<JSAtom*> srcAtom(cx, ToAtom<CanGC>(cx, args[0])); + if (!srcAtom) { + return false; + } + Rooted<PropertyName*> 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<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + if (twoByteSource) { + *length = JS_GetStringLength(linear); + + *twoByteSource = cx->pod_malloc<char16_t>(*length); + if (!*twoByteSource) { + return false; + } + + CopyChars(*twoByteSource, *linear); + } else { + MOZ_ASSERT(utf8Source != nullptr); + + *length = JS::GetDeflatedUTF8StringLength(linear); + + *utf8Source = cx->pod_malloc<char>(*length); + if (!*utf8Source) { + return false; + } + + mozilla::DebugOnly<size_t> 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<JSFunction>() || + !args[1].isObject() || !args[1].toObject().is<JSFunction>()) { + ReportUsageErrorASCII(cx, callee, + "First and second arguments must be functions."); + return false; + } + + mozilla::UniquePtr<ShellSourceHook> hook = + mozilla::MakeUnique<ShellSourceHook>(cx, + args[0].toObject().as<JSFunction>()); + if (!hook) { + return false; + } + + mozilla::UniquePtr<SourceHook> 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<JSContext*>(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<void*> 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<SharedObjectMailbox> SOMailbox; + +// Never null after successful initialization. +static SOMailbox* sharedObjectMailbox; + +static bool InitSharedObjectMailbox() { + sharedObjectMailbox = js_new<SOMailbox>(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<ArrayBufferObjectMaybeShared*> 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<WasmMemoryObject>().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<SharedArrayBufferObject>()) { + Rooted<SharedArrayBufferObject*> sab(cx, + &obj->as<SharedArrayBufferObject>()); + 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<WasmMemoryObject>()) { + // 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<WasmMemoryObject>().isShared()) { + Rooted<SharedArrayBufferObject*> sab( + cx, &obj->as<WasmMemoryObject>() + .buffer() + .as<SharedArrayBufferObject>()); + tag = MailboxTag::WasmMemory; + value.sarb.buffer = sab->rawBufferObject(); + value.sarb.length = sab->byteLength(); + value.sarb.isHugeMemory = obj->as<WasmMemoryObject>().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<uint8_t, 0, SystemAllocPolicy> Uint8Vector; + +class StreamCacheEntry : public AtomicRefCounted<StreamCacheEntry>, + public JS::OptimizedEncodingListener { + typedef AtomicRefCounted<StreamCacheEntry> AtomicBase; + + Uint8Vector bytes_; + ExclusiveData<Uint8Vector> 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<StreamCacheEntry> 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<StreamCacheEntryObject>().cache().Release(); + } + + static bool cachedGetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.thisv().isObject() || + !args.thisv().toObject().is<StreamCacheEntryObject>()) { + return false; + } + + StreamCacheEntryObject& obj = + args.thisv().toObject().as<StreamCacheEntryObject>(); + 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<StreamCacheEntryObject>()) { + return false; + } + + auto& bytes = + args.thisv().toObject().as<StreamCacheEntryObject>().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<uint8_t*> 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<StreamCacheEntry> cache = + cx->new_<StreamCacheEntry>(std::move(bytes)); + if (!cache) { + return false; + } + + Rooted<NativeObject*> obj( + cx, NewObjectWithGivenProto<StreamCacheEntryObject>(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<Uint8Vector, StreamCacheEntryPtr> source; + Thread thread; + JS::StreamConsumer* consumer; + + BufferStreamJob(Uint8Vector&& source, JS::StreamConsumer* consumer) + : source(AsVariant<Uint8Vector>(std::move(source))), consumer(consumer) {} + BufferStreamJob(StreamCacheEntry& source, JS::StreamConsumer* consumer) + : source(AsVariant<StreamCacheEntryPtr>(&source)), consumer(consumer) {} +}; + +struct BufferStreamState { + Vector<UniquePtr<BufferStreamJob>, 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>* bufferStreamState; + +static void BufferStreamMain(BufferStreamJob* job) { + const uint8_t* bytes; + size_t byteLength; + JS::OptimizedEncodingListener* listener; + if (job->source.is<StreamCacheEntryPtr>()) { + StreamCacheEntry& cache = *job->source.as<StreamCacheEntryPtr>(); + 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<Uint8Vector>().begin(); + byteLength = job->source.as<Uint8Vector>().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<JSString*> 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<JSString*> str(cx, mapUrl.toString()); + mapUrlChars = JS_EncodeStringToUTF8(cx, str); + if (!mapUrlChars) { + return false; + } + } + + consumer->noteResponseURLs(urlChars.get(), mapUrlChars.get()); + } + + UniquePtr<BufferStreamJob> job; + + SharedMem<uint8_t*> 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<BufferStreamJob>(std::move(bytes), consumer); + } else if (obj->is<StreamCacheEntryObject>()) { + job = cx->make_unique<BufferStreamJob>( + obj->as<StreamCacheEntryObject>().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<JSFunction>() || + args[0].toObject().is<ShellModuleObjectWrapper>())) { + 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<JSFunction>()) { + RootedFunction fun(cx, &obj->as<JSFunction>()); + 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<ShellModuleObjectWrapper>().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<ArrayObject*>& 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_<MarkBitObservers>(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<ArrayObject*> 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<UniqueChars, 1, js::SystemAllocPolicy> 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<char16_t> 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<TypedArrayObject>().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<JSObject*> ret(cx, FileAsTypedArray(cx, filename)); + if (!ret) { + return false; + } + + Rooted<TypedArrayObject*> typedArray(cx, &ret->as<TypedArrayObject>()); + Rooted<WasmInstanceObject*> 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<JSFunction>()); + + 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<ArrayBufferObject>()) { + ReportUsageErrorASCII(cx, callee, "Argument must be ArrayBuffer."); + return false; + } + + ArrayBufferObject* arrayBuffer = &args[0].toObject().as<ArrayBufferObject>(); + 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<JSObject*> 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<JSLinearString*> 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(<option>, <number>)", +" Set a compiler option indexed in JSCompileOption enum to a number.\n"), +#ifdef DEBUG + JS_FN_HELP("interruptRegexp", InterruptRegexp, 2, 0, +"interruptRegexp(<regexp>, <string>)", +" Interrrupt the execution of regular expression.\n"), +#endif + JS_FN_HELP("enableLastWarning", EnableLastWarning, 0, 0, +"enableLastWarning()", +" Enable storing the last warning."), + + JS_FN_HELP("disableLastWarning", DisableLastWarning, 0, 0, +"disableLastWarning()", +" Disable storing the last warning."), + + JS_FN_HELP("getLastWarning", GetLastWarning, 0, 0, +"getLastWarning()", +" Returns an object that represents the last warning."), + + JS_FN_HELP("clearLastWarning", ClearLastWarning, 0, 0, +"clearLastWarning()", +" Clear the last warning."), + + JS_FN_HELP("elapsed", Elapsed, 0, 0, +"elapsed()", +" Execution time elapsed for the current thread."), + + JS_FN_HELP("decompileFunction", DecompileFunction, 1, 0, +"decompileFunction(func)", +" Decompile a function."), + + JS_FN_HELP("decompileThis", DecompileThisScript, 0, 0, +"decompileThis()", +" Decompile the currently executing script."), + + JS_FN_HELP("valueToSource", ValueToSource, 1, 0, +"valueToSource(value)", +" Format a value for inspection."), + + JS_FN_HELP("thisFilename", ThisFilename, 0, 0, +"thisFilename()", +" Return the filename of the current script"), + + JS_FN_HELP("newGlobal", NewGlobal, 1, 0, +"newGlobal([options])", +" Return a new global object/realm. The new global is created in the\n" +" 'newGlobal' function object's compartment and zone, unless the\n" +" '--more-compartments' command-line flag was given, in which case new\n" +" globals get a fresh compartment and zone. If options is given, it may\n" +" have any of the following properties:\n" +" sameCompartmentAs: If an object, the global will be in the same\n" +" compartment and zone as the given object.\n" +" sameZoneAs: The global will be in a new compartment in the same zone\n" +" as the given object.\n" +" newCompartment: If true, the global will always be created in a new\n" +" compartment and zone.\n" +" invisibleToDebugger: If true, the global will be invisible to the\n" +" debugger (default false)\n" +" discardSource: If true, discard source after compiling a script\n" +" (default false).\n" +" useWindowProxy: the global will be created with a WindowProxy attached. In this\n" +" case, the WindowProxy will be returned.\n" +" freezeBuiltins: certain builtin constructors will be frozen when created and\n" +" their prototypes will be sealed. These constructors will be defined on the\n" +" global as non-configurable and non-writable.\n" +" immutablePrototype: whether the global's prototype is immutable.\n" +" principal: if present, its value converted to a number must be an\n" +" integer that fits in 32 bits; use that as the new realm's\n" +" principal. Shell principals are toys, meant only for testing; one\n" +" shell principal subsumes another if its set bits are a superset of\n" +" the other's. Thus, a principal of 0 subsumes nothing, while a\n" +" principals of ~0 subsumes all other principals. The absence of a\n" +" principal is treated as if its bits were 0xffff, for subsumption\n" +" purposes. If this property is omitted, supply no principal.\n" +" systemPrincipal: If true, use the shell's trusted principals for the\n" +" new realm. This creates a realm that's marked as a 'system' realm."), + + JS_FN_HELP("nukeAllCCWs", NukeAllCCWs, 0, 0, +"nukeAllCCWs()", +" Like nukeCCW, but for all CrossCompartmentWrappers targeting the current realm."), + + JS_FN_HELP("recomputeWrappers", RecomputeWrappers, 2, 0, +"recomputeWrappers([src, [target]])", +" Recompute all cross-compartment wrappers. src and target are both optional\n" +" and can be used to filter source or target compartments: the unwrapped\n" +" object's compartment is used as CompartmentFilter.\n"), + + JS_FN_HELP("dumpObjectWrappers", DumpObjectWrappers, 2, 0, +"dumpObjectWrappers()", +" Print information about cross-compartment object wrappers.\n"), + + JS_FN_HELP("wrapWithProto", WrapWithProto, 2, 0, +"wrapWithProto(obj)", +" Wrap an object into a noop wrapper with prototype semantics."), + + JS_FN_HELP("createExternalArrayBuffer", CreateExternalArrayBuffer, 1, 0, +"createExternalArrayBuffer(size)", +" Create an array buffer that has external data of size."), + + JS_FN_HELP("createMappedArrayBuffer", CreateMappedArrayBuffer, 1, 0, +"createMappedArrayBuffer(filename, [offset, [size]])", +" Create an array buffer that mmaps the given file."), + + JS_FN_HELP("addPromiseReactions", AddPromiseReactions, 3, 0, +"addPromiseReactions(promise, onResolve, onReject)", +" Calls the JS::AddPromiseReactions JSAPI function with the given arguments."), + + JS_FN_HELP("ignoreUnhandledRejections", IgnoreUnhandledRejections, 0, 0, +"ignoreUnhandledRejections()", +" By default, js shell tracks unhandled promise rejections and reports\n" +" them at the end of the exectuion. If a testcase isn't interested\n" +" in those rejections, call this to stop tracking and reporting."), + + JS_FN_HELP("getMaxArgs", GetMaxArgs, 0, 0, +"getMaxArgs()", +" Return the maximum number of supported args for a call."), + + JS_FN_HELP("createIsHTMLDDA", CreateIsHTMLDDA, 0, 0, +"createIsHTMLDDA()", +" Return an object |obj| that \"looks like\" the |document.all| object in\n" +" browsers in certain ways: |typeof obj === \"undefined\"|, |obj == null|\n" +" and |obj == undefined| (vice versa for !=), |ToBoolean(obj) === false|,\n" +" and when called with no arguments or the single argument \"\" returns\n" +" null. (Calling |obj| any other way crashes or throws an exception.)\n" +" This function implements the exact requirements of the $262.IsHTMLDDA\n" +" property in test262."), + + JS_FN_HELP("cacheEntry", CacheEntry, 1, 0, +"cacheEntry(code)", +" Return a new opaque object which emulates a cache entry of a script. This\n" +" object encapsulates the code and its cached content. The cache entry is filled\n" +" and read by the \"evaluate\" function by using it in place of the source, and\n" +" by setting \"saveIncrementalBytecode\" and \"loadBytecode\" options."), + + JS_FN_HELP("streamCacheEntry", StreamCacheEntryObject::construct, 1, 0, +"streamCacheEntry(buffer)", +" Create a shell-only object that holds wasm bytecode and can be streaming-\n" +" compiled and cached by WebAssembly.{compile,instantiate}Streaming(). On a\n" +" second compilation of the same cache entry, the cached code will be used."), + + JS_FN_HELP("printProfilerEvents", PrintProfilerEvents, 0, 0, +"printProfilerEvents()", +" Register a callback with the profiler that prints javascript profiler events\n" +" to stderr. Callback is only registered if profiling is enabled."), + + JS_FN_HELP("enableSingleStepProfiling", EnableSingleStepProfiling, 0, 0, +"enableSingleStepProfiling()", +" This function will fail on platforms that don't support single-step profiling\n" +" (currently ARM and MIPS64 support it). When enabled, at every instruction a\n" +" backtrace will be recorded and stored in an array. Adjacent duplicate backtraces\n" +" are discarded."), + + JS_FN_HELP("disableSingleStepProfiling", DisableSingleStepProfiling, 0, 0, +"disableSingleStepProfiling()", +" Return the array of backtraces recorded by enableSingleStepProfiling."), + + JS_FN_HELP("enableGeckoProfiling", EnableGeckoProfiling, 0, 0, +"enableGeckoProfiling()", +" Enables Gecko Profiler instrumentation and corresponding assertions, with slow\n" +" assertions disabled.\n"), + + JS_FN_HELP("enableGeckoProfilingWithSlowAssertions", EnableGeckoProfilingWithSlowAssertions, 0, 0, +"enableGeckoProfilingWithSlowAssertions()", +" Enables Gecko Profiler instrumentation and corresponding assertions, with slow\n" +" assertions enabled.\n"), + + JS_FN_HELP("disableGeckoProfiling", DisableGeckoProfiling, 0, 0, +"disableGeckoProfiling()", +" Disables Gecko Profiler instrumentation"), + + JS_FN_HELP("isLatin1", IsLatin1, 1, 0, +"isLatin1(s)", +" Return true iff the string's characters are stored as Latin1."), + + JS_FN_HELP("stackPointerInfo", StackPointerInfo, 0, 0, +"stackPointerInfo()", +" Return an int32 value which corresponds to the offset of the latest stack\n" +" pointer, such that one can take the differences of 2 to estimate a frame-size."), + + JS_FN_HELP("entryPoints", EntryPoints, 1, 0, +"entryPoints(params)", +"Carry out some JSAPI operation as directed by |params|, and return an array of\n" +"objects describing which JavaScript entry points were invoked as a result.\n" +"|params| is an object whose properties indicate what operation to perform. Here\n" +"are the recognized groups of properties:\n" +"\n" +"{ function }: Call the object |params.function| with no arguments.\n" +"\n" +"{ object, property }: Fetch the property named |params.property| of\n" +"|params.object|.\n" +"\n" +"{ ToString }: Apply JS::ToString to |params.toString|.\n" +"\n" +"{ ToNumber }: Apply JS::ToNumber to |params.toNumber|.\n" +"\n" +"{ eval }: Apply JS::Evaluate to |params.eval|.\n" +"\n" +"The return value is an array of strings, with one element for each\n" +"JavaScript invocation that occurred as a result of the given\n" +"operation. Each element is the name of the function invoked, or the\n" +"string 'eval:FILENAME' if the code was invoked by 'eval' or something\n" +"similar.\n"), + + JS_FN_HELP("enqueueJob", EnqueueJob, 1, 0, +"enqueueJob(fn)", +" Enqueue 'fn' on the shell's job queue."), + + JS_FN_HELP("globalOfFirstJobInQueue", GlobalOfFirstJobInQueue, 0, 0, +"globalOfFirstJobInQueue()", +" Returns the global of the first item in the job queue. Throws an exception\n" +" if the queue is empty.\n"), + + JS_FN_HELP("drainJobQueue", DrainJobQueue, 0, 0, +"drainJobQueue()", +"Take jobs from the shell's job queue in FIFO order and run them until the\n" +"queue is empty.\n"), + + JS_FN_HELP("setPromiseRejectionTrackerCallback", SetPromiseRejectionTrackerCallback, 1, 0, +"setPromiseRejectionTrackerCallback()", +"Sets the callback to be invoked whenever a Promise rejection is unhandled\n" +"or a previously-unhandled rejection becomes handled."), + + JS_FN_HELP("dumpScopeChain", DumpScopeChain, 1, 0, +"dumpScopeChain(obj)", +" Prints the scope chain of an interpreted function or a module."), + + JS_FN_HELP("blackRoot", EnsureBlackRoot, 0, 0, +"blackRoot()", +" Return an array in the current compartment whose elements will be marked\n" +" as black roots by the GC."), + + JS_FN_HELP("grayRoot", EnsureGrayRoot, 0, 0, +"grayRoot()", +" Return an array in the current compartment whose elements will be marked\n" +" as gray roots by the GC."), + + JS_FN_HELP("addMarkObservers", AddMarkObservers, 1, 0, +"addMarkObservers(array_of_objects)", +" Register an array of objects whose mark bits will be tested by calls to\n" +" getMarks. The objects will be in calling compartment. Objects from\n" +" multiple compartments may be monitored by calling this function in\n" +" different compartments."), + + JS_FN_HELP("clearMarkObservers", ClearMarkObservers, 1, 0, +"clearMarkObservers()", +" Clear out the list of objects whose mark bits will be tested.\n"), + + JS_FN_HELP("getMarks", GetMarks, 0, 0, +"getMarks()", +" Return an array of strings representing the current state of the mark\n" +" bits ('gray' or 'black', or 'dead' if the object has been collected)\n" +" for the objects registered via addMarkObservers. Note that some of the\n" +" objects tested may be from different compartments than the one in which\n" +" this function runs."), + + JS_FN_HELP("bindToAsyncStack", BindToAsyncStack, 2, 0, +"bindToAsyncStack(fn, { stack, cause, explicit })", +" Returns a new function that calls 'fn' with no arguments, passing\n" +" 'undefined' as the 'this' value, and supplies an async stack for the\n" +" call as described by the second argument, an object with the following\n" +" properties (which are not optional, unless specified otherwise):\n" +"\n" +" stack: A SavedFrame object, like that returned by 'saveStack'. Stacks\n" +" captured during calls to the returned function capture this as\n" +" their async stack parent, accessible via a SavedFrame's\n" +" 'asyncParent' property.\n" +"\n" +" cause: A string, supplied as the async cause on the top frame of\n" +" captured async stacks.\n" +"\n" +" explicit: A boolean value, indicating whether the given 'stack' should\n" +" always supplant the returned function's true callers (true),\n" +" or only when there are no other JavaScript frames on the stack\n" +" below it (false). If omitted, this is treated as 'true'."), + +#ifdef JS_HAS_INTL_API + JS_FN_HELP("addIntlExtras", AddIntlExtras, 1, 0, +"addIntlExtras(obj)", +"Adds various not-yet-standardized Intl functions as properties on the\n" +"provided object (this should generally be Intl itself). The added\n" +"functions and their behavior are experimental: don't depend upon them\n" +"unless you're willing to update your code if these experimental APIs change\n" +"underneath you."), +#endif // JS_HAS_INTL_API + +#ifndef __wasi__ + JS_FN_HELP("wasmCompileInSeparateProcess", WasmCompileInSeparateProcess, 1, 0, +"wasmCompileInSeparateProcess(buffer)", +" Compile the given buffer in a separate process, serialize the resulting\n" +" wasm::Module into bytes, and deserialize those bytes in the current\n" +" process, returning the resulting WebAssembly.Module."), + + JS_FN_HELP("wasmTextToBinary", WasmTextToBinary, 1, 0, +"wasmTextToBinary(str)", +" Translates the given text wasm module into its binary encoding."), +#endif // __wasi__ + + JS_FN_HELP("transplantableObject", TransplantableObject, 0, 0, +"transplantableObject([options])", +" Returns the pair {object, transplant}. |object| is an object which can be\n" +" transplanted into a new object when the |transplant| function, which must\n" +" be invoked with a global object, is called.\n" +" |object| is swapped with a cross-compartment wrapper if the global object\n" +" is in a different compartment.\n" +"\n" +" If options is given, it may have any of the following properties:\n" +" proxy: Create a DOM Proxy object instead of a plain DOM object.\n" +" object: Don't create a new DOM object, but instead use the supplied\n" +" FakeDOMObject."), + + JS_FN_HELP("cpuNow", CpuNow, /* nargs= */ 0, /* flags = */ 0, +"cpuNow()", +" Returns the approximate processor time used by the process since an arbitrary epoch, in seconds.\n" +" Only the difference between two calls to `cpuNow()` is meaningful."), + +#ifdef FUZZING_JS_FUZZILLI + JS_FN_HELP("fuzzilli", Fuzzilli, 0, 0, +"fuzzilli(operation, arg)", +" Exposes functionality used by the Fuzzilli JavaScript fuzzer."), +#endif + +#ifdef FUZZING_INTERFACES + JS_FN_HELP("getWasmSmithModule", GetWasmSmithModule, 1, 0, +"getWasmSmithModule(arrayBuffer)", +" Call wasm-smith to generate a random wasm module from the provided data."), +#endif + + JS_FN_HELP("isValidJSON", IsValidJSON, 1, 0, +"isValidJSON(source)", +" Returns true if the given source is valid JSON."), + + JS_FS_HELP_END +}; +// clang-format on + +// clang-format off +#ifdef FUZZING_JS_FUZZILLI +static const JSFunctionSpec shell_function_fuzzilli_hash[] = { + JS_INLINABLE_FN("fuzzilli_hash", fuzzilli_hash, 1, 0, FuzzilliHash), + JS_FS_END +}; +#endif +// clang-format on + +// clang-format off +static const JSFunctionSpecWithHelp diff_testing_unsafe_functions[] = { + + JS_FS_HELP_END +}; +// clang-format on + +// clang-format off +static const JSFunctionSpecWithHelp fuzzing_unsafe_functions[] = { + JS_FN_HELP("getSelfHostedValue", GetSelfHostedValue, 1, 0, +"getSelfHostedValue()", +" Get a self-hosted value by its name. Note that these values don't get \n" +" cached, so repeatedly getting the same value creates multiple distinct clones."), + + JS_FN_HELP("line2pc", LineToPC, 0, 0, +"line2pc([fun,] line)", +" Map line number to PC."), + + JS_FN_HELP("pc2line", PCToLine, 0, 0, +"pc2line(fun[, pc])", +" Map PC to line number."), + + JS_INLINABLE_FN_HELP("assertFloat32", testingFunc_assertFloat32, 2, 0, TestAssertFloat32, +"assertFloat32(value, isFloat32)", +" In IonMonkey only, asserts that value has (resp. hasn't) the MIRType::Float32 if isFloat32 is true (resp. false)."), + + JS_INLINABLE_FN_HELP("assertRecoveredOnBailout", testingFunc_assertRecoveredOnBailout, 2, 0, +TestAssertRecoveredOnBailout, +"assertRecoveredOnBailout(var)", +" In IonMonkey only, asserts that variable has RecoveredOnBailout flag."), + + JS_FN_HELP("withSourceHook", WithSourceHook, 1, 0, +"withSourceHook(hook, fun)", +" Set this JS runtime's lazy source retrieval hook (that is, the hook\n" +" used to find sources compiled with |CompileOptions::LAZY_SOURCE|) to\n" +" |hook|; call |fun| with no arguments; and then restore the runtime's\n" +" original hook. Return or throw whatever |fun| did. |hook| gets\n" +" passed the requested code's URL, and should return a string.\n" +"\n" +" Notes:\n" +"\n" +" 1) SpiderMonkey may assert if the returned code isn't close enough\n" +" to the script's real code, so this function is not fuzzer-safe.\n" +"\n" +" 2) The runtime can have only one source retrieval hook active at a\n" +" time. If |fun| is not careful, |hook| could be asked to retrieve the\n" +" source code for compilations that occurred long before it was set,\n" +" and that it knows nothing about. The reverse applies as well: the\n" +" original hook, that we reinstate after the call to |fun| completes,\n" +" might be asked for the source code of compilations that |fun|\n" +" performed, and which, presumably, only |hook| knows how to find.\n"), + + JS_FN_HELP("crash", Crash, 0, 0, +"crash([message, [{disable_minidump:true}]])", +" Crashes the process with a MOZ_CRASH, optionally providing a message.\n" +" An options object may be passed as the second argument. If the key\n" +" 'suppress_minidump' is set to true, then a minidump will not be\n" +" generated by the crash (which only has an effect if the breakpad\n" +" dumping library is loaded.)"), + +#ifndef __wasi__ + JS_FN_HELP("wasmLoop", WasmLoop, 2, 0, +"wasmLoop(filename, imports)", +" Performs an AFL-style persistent loop reading data from the given file and passing it\n" +" to the 'wasmEval' function together with the specified imports object."), +#endif // __wasi__ + + JS_FN_HELP("setBufferStreamParams", SetBufferStreamParams, 2, 0, +"setBufferStreamParams(delayMillis, chunkByteSize)", +" Set the delay time (between calls to StreamConsumer::consumeChunk) and chunk\n" +" size (in bytes)."), + +#ifdef JS_CACHEIR_SPEW + JS_FN_HELP("cacheIRHealthReport", CacheIRHealthReport, 0, 0, +"cacheIRHealthReport()", +" Show health rating of CacheIR stubs."), +#endif + +#ifdef DEBUG + JS_FN_HELP("debugGetQueuedJobs", DebugGetQueuedJobs, 0, 0, +"debugGetQueuedJobs()", +" Returns an array of queued jobs."), +#endif + + JS_FS_HELP_END +}; +// clang-format on + +// clang-format off +static const JSFunctionSpecWithHelp performance_functions[] = { + JS_FN_HELP("now", Now, 0, 0, +"now()", +" Return the current time with sub-ms precision.\n" +" This function is an alias of the dateNow() function."), + JS_FS_HELP_END +}; +// clang-format on + +// clang-format off +static const JSFunctionSpecWithHelp console_functions[] = { + JS_FN_HELP("log", Print, 0, 0, +"log([exp ...])", +" Evaluate and print expressions to stdout.\n" +" This function is an alias of the print() function."), + JS_FS_HELP_END +}; +// clang-format on + +bool DefineConsole(JSContext* cx, HandleObject global) { + RootedObject obj(cx, JS_NewPlainObject(cx)); + return obj && JS_DefineFunctionsWithHelp(cx, obj, console_functions) && + JS_DefineProperty(cx, global, "console", obj, 0); +} + +#ifdef MOZ_PROFILING +# define PROFILING_FUNCTION_COUNT 5 +# ifdef MOZ_CALLGRIND +# define CALLGRIND_FUNCTION_COUNT 3 +# else +# define CALLGRIND_FUNCTION_COUNT 0 +# endif +# ifdef MOZ_VTUNE +# define VTUNE_FUNCTION_COUNT 4 +# else +# define VTUNE_FUNCTION_COUNT 0 +# endif +# define EXTERNAL_FUNCTION_COUNT \ + (PROFILING_FUNCTION_COUNT + CALLGRIND_FUNCTION_COUNT + VTUNE_FUNCTION_COUNT) +#else +# define EXTERNAL_FUNCTION_COUNT 0 +#endif + +#undef PROFILING_FUNCTION_COUNT +#undef CALLGRIND_FUNCTION_COUNT +#undef VTUNE_FUNCTION_COUNT +#undef EXTERNAL_FUNCTION_COUNT + +static bool PrintHelpString(JSContext* cx, HandleValue v) { + RootedString str(cx, v.toString()); + MOZ_ASSERT(gOutFile->isOpen()); + + UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); + if (!bytes) { + return false; + } + + fprintf(gOutFile->fp, "%s\n", bytes.get()); + return true; +} + +static bool PrintHelp(JSContext* cx, HandleObject obj) { + RootedValue usage(cx); + if (!JS_GetProperty(cx, obj, "usage", &usage)) { + return false; + } + RootedValue help(cx); + if (!JS_GetProperty(cx, obj, "help", &help)) { + return false; + } + + if (!usage.isString() || !help.isString()) { + return true; + } + + return PrintHelpString(cx, usage) && PrintHelpString(cx, help); +} + +static bool PrintEnumeratedHelp(JSContext* cx, HandleObject obj, + HandleObject pattern, bool brief) { + RootedIdVector idv(cx); + if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY | JSITER_HIDDEN, &idv)) { + return false; + } + + Rooted<RegExpObject*> regex(cx); + if (pattern) { + regex = &UncheckedUnwrap(pattern)->as<RegExpObject>(); + } + + for (size_t i = 0; i < idv.length(); i++) { + RootedValue v(cx); + RootedId id(cx, idv[i]); + if (!JS_GetPropertyById(cx, obj, id, &v)) { + return false; + } + if (!v.isObject()) { + continue; + } + + RootedObject funcObj(cx, &v.toObject()); + if (regex) { + // Only pay attention to objects with a 'help' property, which will + // either be documented functions or interface objects. + if (!JS_GetProperty(cx, funcObj, "help", &v)) { + return false; + } + if (!v.isString()) { + continue; + } + + // For functions, match against the name. For interface objects, + // match against the usage string. + if (!JS_GetProperty(cx, funcObj, "name", &v)) { + return false; + } + if (!v.isString()) { + if (!JS_GetProperty(cx, funcObj, "usage", &v)) { + return false; + } + if (!v.isString()) { + continue; + } + } + + Rooted<JSString*> inputStr(cx, v.toString()); + if (!inputStr->ensureLinear(cx)) { + return false; + } + + // Execute the regular expression in |regex|'s compartment. + AutoRealm ar(cx, regex); + if (!cx->compartment()->wrap(cx, &inputStr)) { + return false; + } + Rooted<JSLinearString*> input(cx, &inputStr->asLinear()); + size_t ignored = 0; + if (!ExecuteRegExpLegacy(cx, nullptr, regex, input, &ignored, true, &v)) { + return false; + } + if (v.isNull()) { + continue; + } + } + + if (!PrintHelp(cx, funcObj)) { + return false; + } + } + + return true; +} + +static bool Help(JSContext* cx, unsigned argc, Value* vp) { + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + + // help() - display the version and dump out help for all functions on the + // global. + if (args.length() == 0) { + fprintf(gOutFile->fp, "%s\n", JS_GetImplementationVersion()); + + if (!PrintEnumeratedHelp(cx, global, nullptr, false)) { + return false; + } + return true; + } + + RootedValue v(cx); + + if (args[0].isPrimitive()) { + // help("foo") + JS_ReportErrorASCII(cx, "primitive arg"); + return false; + } + + RootedObject obj(cx, &args[0].toObject()); + if (!obj) { + return true; + } + bool isRegexp; + if (!JS::ObjectIsRegExp(cx, obj, &isRegexp)) { + return false; + } + + if (isRegexp) { + // help(/pattern/) + return PrintEnumeratedHelp(cx, global, obj, false); + } + + // help(function) + // help(namespace_obj) + return PrintHelp(cx, obj); +} + +static const JSErrorFormatString jsShell_ErrorFormatString[JSShellErr_Limit] = { +#define MSG_DEF(name, count, exception, format) \ + {#name, format, count, JSEXN_ERR}, +#include "jsshell.msg" +#undef MSG_DEF +}; + +const JSErrorFormatString* js::shell::my_GetErrorMessage( + void* userRef, const unsigned errorNumber) { + if (errorNumber == 0 || errorNumber >= JSShellErr_Limit) { + return nullptr; + } + + return &jsShell_ErrorFormatString[errorNumber]; +} + +static bool CreateLastWarningObject(JSContext* cx, JSErrorReport* report) { + RootedObject warningObj(cx, JS_NewObject(cx, nullptr)); + if (!warningObj) { + return false; + } + + if (!CopyErrorReportToObject(cx, report, warningObj)) { + return false; + } + + GetShellContext(cx)->lastWarning.setObject(*warningObj); + return true; +} + +static FILE* ErrorFilePointer() { + if (gErrFile->isOpen()) { + return gErrFile->fp; + } + + fprintf(stderr, "error file is closed; falling back to stderr\n"); + return stderr; +} + +bool shell::PrintStackTrace(JSContext* cx, HandleObject stackObj) { + if (!stackObj || !stackObj->is<SavedFrame>()) { + return true; + } + + JSPrincipals* principals = stackObj->nonCCWRealm()->principals(); + RootedString stackStr(cx); + if (!BuildStackString(cx, principals, stackObj, &stackStr, 2)) { + return false; + } + + UniqueChars stack = JS_EncodeStringToUTF8(cx, stackStr); + if (!stack) { + return false; + } + + FILE* fp = ErrorFilePointer(); + fputs("Stack:\n", fp); + fputs(stack.get(), fp); + + return true; +} + +js::shell::AutoReportException::~AutoReportException() { + if (!JS_IsExceptionPending(cx)) { + return; + } + + auto printError = [](JSContext* cx, auto& report, const auto& exnStack, + const char* prefix = nullptr) { + if (!report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) { + fprintf(stderr, "out of memory initializing JS::ErrorReportBuilder\n"); + fflush(stderr); + JS_ClearPendingException(cx); + return false; + } + + MOZ_ASSERT(!report.report()->isWarning()); + + FILE* fp = ErrorFilePointer(); + if (prefix) { + fputs(prefix, fp); + } + JS::PrintError(fp, report, reportWarnings); + JS_ClearPendingException(cx); + + // If possible, use the original error stack as the source of truth, because + // finally block handlers may have overwritten the exception stack. + RootedObject stack(cx, exnStack.stack()); + if (exnStack.exception().isObject()) { + RootedObject exception(cx, &exnStack.exception().toObject()); + if (JSObject* exceptionStack = JS::ExceptionStackOrNull(exception)) { + stack.set(exceptionStack); + } + } + + if (!PrintStackTrace(cx, stack)) { + fputs("(Unable to print stack trace)\n", fp); + JS_ClearPendingException(cx); + } + + return true; + }; + + // Get exception object and stack before printing and clearing exception. + JS::ExceptionStack exnStack(cx); + if (!JS::StealPendingExceptionStack(cx, &exnStack)) { + fprintf(stderr, "out of memory while stealing exception\n"); + fflush(stderr); + JS_ClearPendingException(cx); + return; + } + + ShellContext* sc = GetShellContext(cx); + JS::ErrorReportBuilder report(cx); + if (!printError(cx, report, exnStack)) { + // Return if we couldn't initialize the error report. + return; + } + + // Print the error's cause, if available. + if (exnStack.exception().isObject()) { + JSObject* exception = &exnStack.exception().toObject(); + if (exception->is<ErrorObject>()) { + auto* error = &exception->as<ErrorObject>(); + if (auto maybeCause = error->getCause()) { + RootedValue cause(cx, maybeCause.value()); + + RootedObject causeStack(cx); + if (cause.isObject()) { + RootedObject causeObj(cx, &cause.toObject()); + causeStack = JS::ExceptionStackOrNull(causeObj); + } + + JS::ExceptionStack exnStack(cx, cause, causeStack); + JS::ErrorReportBuilder report(cx); + printError(cx, report, exnStack, "Caused by: "); + } + } + } + +#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) + // Don't quit the shell if an unhandled exception is reported during OOM + // testing. + if (cx->runningOOMTest) { + return; + } +#endif + + if (report.report()->errorNumber == JSMSG_OUT_OF_MEMORY) { + sc->exitCode = EXITCODE_OUT_OF_MEMORY; + } else { + sc->exitCode = EXITCODE_RUNTIME_ERROR; + } +} + +void js::shell::WarningReporter(JSContext* cx, JSErrorReport* report) { + ShellContext* sc = GetShellContext(cx); + FILE* fp = ErrorFilePointer(); + + MOZ_ASSERT(report->isWarning()); + + if (sc->lastWarningEnabled) { + JS::AutoSaveExceptionState savedExc(cx); + if (!CreateLastWarningObject(cx, report)) { + fputs("Unhandled error happened while creating last warning object.\n", + fp); + fflush(fp); + } + savedExc.restore(); + } + + // Print the warning. + JS::PrintError(fp, report, reportWarnings); +} + +static bool global_enumerate(JSContext* cx, JS::HandleObject obj, + JS::MutableHandleIdVector properties, + bool enumerableOnly) { +#ifdef LAZY_STANDARD_CLASSES + return JS_NewEnumerateStandardClasses(cx, obj, properties, enumerableOnly); +#else + return true; +#endif +} + +static bool global_resolve(JSContext* cx, HandleObject obj, HandleId id, + bool* resolvedp) { +#ifdef LAZY_STANDARD_CLASSES + if (!JS_ResolveStandardClass(cx, obj, id, resolvedp)) { + return false; + } +#endif + return true; +} + +static bool global_mayResolve(const JSAtomState& names, jsid id, + JSObject* maybeObj) { + return JS_MayResolveStandardClass(names, id, maybeObj); +} + +static const JSClassOps global_classOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + global_enumerate, // newEnumerate + global_resolve, // resolve + global_mayResolve, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + JS_GlobalObjectTraceHook, // trace +}; + +static constexpr uint32_t DOM_PROTOTYPE_SLOT = JSCLASS_GLOBAL_SLOT_COUNT; +static constexpr uint32_t DOM_GLOBAL_SLOTS = 1; + +static const JSClass global_class = { + "global", + JSCLASS_GLOBAL_FLAGS | JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(DOM_GLOBAL_SLOTS), + &global_classOps}; + +/* + * Define a FakeDOMObject constructor. It returns an object with a getter, + * setter and method with attached JitInfo. This object can be used to test + * IonMonkey DOM optimizations in the shell. + */ + +/* Fow now just use to a constant we can check. */ +static const void* DOM_PRIVATE_VALUE = (void*)0x1234; + +static bool dom_genericGetter(JSContext* cx, unsigned argc, JS::Value* vp); + +static bool dom_genericSetter(JSContext* cx, unsigned argc, JS::Value* vp); + +static bool dom_genericMethod(JSContext* cx, unsigned argc, JS::Value* vp); + +static bool dom_get_x(JSContext* cx, HandleObject obj, void* self, + JSJitGetterCallArgs args) { + MOZ_ASSERT(JS::GetClass(obj) == GetDomClass()); + MOZ_ASSERT(self == DOM_PRIVATE_VALUE); + args.rval().set(JS_NumberValue(double(3.14))); + return true; +} + +static bool dom_set_x(JSContext* cx, HandleObject obj, void* self, + JSJitSetterCallArgs args) { + MOZ_ASSERT(JS::GetClass(obj) == GetDomClass()); + MOZ_ASSERT(self == DOM_PRIVATE_VALUE); + return true; +} + +static bool dom_get_slot(JSContext* cx, HandleObject obj, void* self, + JSJitGetterCallArgs args) { + MOZ_ASSERT(JS::GetClass(obj) == GetDomClass()); + MOZ_ASSERT(self == DOM_PRIVATE_VALUE); + + Value v = JS::GetReservedSlot(obj, DOM_OBJECT_SLOT2); + MOZ_ASSERT(v.toInt32() == 42); + args.rval().set(v); + return true; +} + +static bool dom_get_global(JSContext* cx, HandleObject obj, void* self, + JSJitGetterCallArgs args) { + MOZ_ASSERT(JS::GetClass(obj) == GetDomClass()); + MOZ_ASSERT(self == DOM_PRIVATE_VALUE); + + // Return the current global (instead of obj->global()) to test cx->realm + // switching in the JIT. + args.rval().setObject(*ToWindowProxyIfWindow(cx->global())); + + return true; +} + +static bool dom_set_global(JSContext* cx, HandleObject obj, void* self, + JSJitSetterCallArgs args) { + MOZ_ASSERT(JS::GetClass(obj) == GetDomClass()); + MOZ_ASSERT(self == DOM_PRIVATE_VALUE); + + // Throw an exception if our argument is not the current global. This lets + // us test cx->realm switching. + if (!args[0].isObject() || + ToWindowIfWindowProxy(&args[0].toObject()) != cx->global()) { + JS_ReportErrorASCII(cx, "Setter not called with matching global argument"); + return false; + } + + return true; +} + +static bool dom_doFoo(JSContext* cx, HandleObject obj, void* self, + const JSJitMethodCallArgs& args) { + MOZ_ASSERT(JS::GetClass(obj) == GetDomClass()); + MOZ_ASSERT(self == DOM_PRIVATE_VALUE); + MOZ_ASSERT(cx->realm() == args.callee().as<JSFunction>().realm()); + + /* Just return args.length(). */ + args.rval().setInt32(args.length()); + return true; +} + +static const JSJitInfo dom_x_getterinfo = { + {(JSJitGetterOp)dom_get_x}, + {0}, /* protoID */ + {0}, /* depth */ + JSJitInfo::Getter, + JSJitInfo::AliasNone, /* aliasSet */ + JSVAL_TYPE_UNKNOWN, /* returnType */ + true, /* isInfallible. False in setters. */ + true, /* isMovable */ + true, /* isEliminatable */ + false, /* isAlwaysInSlot */ + false, /* isLazilyCachedInSlot */ + false, /* isTypedMethod */ + 0 /* slotIndex */ +}; + +static const JSJitInfo dom_x_setterinfo = { + {(JSJitGetterOp)dom_set_x}, + {0}, /* protoID */ + {0}, /* depth */ + JSJitInfo::Setter, + JSJitInfo::AliasEverything, /* aliasSet */ + JSVAL_TYPE_UNKNOWN, /* returnType */ + false, /* isInfallible. False in setters. */ + false, /* isMovable. */ + false, /* isEliminatable. */ + false, /* isAlwaysInSlot */ + false, /* isLazilyCachedInSlot */ + false, /* isTypedMethod */ + 0 /* slotIndex */ +}; + +static const JSJitInfo dom_slot_getterinfo = { + {(JSJitGetterOp)dom_get_slot}, + {0}, /* protoID */ + {0}, /* depth */ + JSJitInfo::Getter, + JSJitInfo::AliasNone, /* aliasSet */ + JSVAL_TYPE_INT32, /* returnType */ + false, /* isInfallible. False in setters. */ + true, /* isMovable */ + true, /* isEliminatable */ + true, /* isAlwaysInSlot */ + false, /* isLazilyCachedInSlot */ + false, /* isTypedMethod */ + DOM_OBJECT_SLOT2 /* slotIndex */ +}; + +// Note: this getter uses AliasEverything and is marked as fallible and +// non-movable (1) to prevent Ion from getting too clever optimizing it and +// (2) it's nice to have a few different kinds of getters in the shell. +static const JSJitInfo dom_global_getterinfo = { + {(JSJitGetterOp)dom_get_global}, + {0}, /* protoID */ + {0}, /* depth */ + JSJitInfo::Getter, + JSJitInfo::AliasEverything, /* aliasSet */ + JSVAL_TYPE_OBJECT, /* returnType */ + false, /* isInfallible. False in setters. */ + false, /* isMovable */ + false, /* isEliminatable */ + false, /* isAlwaysInSlot */ + false, /* isLazilyCachedInSlot */ + false, /* isTypedMethod */ + 0 /* slotIndex */ +}; + +static const JSJitInfo dom_global_setterinfo = { + {(JSJitGetterOp)dom_set_global}, + {0}, /* protoID */ + {0}, /* depth */ + JSJitInfo::Setter, + JSJitInfo::AliasEverything, /* aliasSet */ + JSVAL_TYPE_UNKNOWN, /* returnType */ + false, /* isInfallible. False in setters. */ + false, /* isMovable. */ + false, /* isEliminatable. */ + false, /* isAlwaysInSlot */ + false, /* isLazilyCachedInSlot */ + false, /* isTypedMethod */ + 0 /* slotIndex */ +}; + +static const JSJitInfo doFoo_methodinfo = { + {(JSJitGetterOp)dom_doFoo}, + {0}, /* protoID */ + {0}, /* depth */ + JSJitInfo::Method, + JSJitInfo::AliasEverything, /* aliasSet */ + JSVAL_TYPE_UNKNOWN, /* returnType */ + false, /* isInfallible. False in setters. */ + false, /* isMovable */ + false, /* isEliminatable */ + false, /* isAlwaysInSlot */ + false, /* isLazilyCachedInSlot */ + false, /* isTypedMethod */ + 0 /* slotIndex */ +}; + +static const JSPropertySpec dom_props[] = { + JSPropertySpec::nativeAccessors("x", JSPROP_ENUMERATE, dom_genericGetter, + &dom_x_getterinfo, dom_genericSetter, + &dom_x_setterinfo), + JSPropertySpec::nativeAccessors("slot", JSPROP_ENUMERATE, dom_genericGetter, + &dom_slot_getterinfo), + JSPropertySpec::nativeAccessors("global", JSPROP_ENUMERATE, + dom_genericGetter, &dom_global_getterinfo, + dom_genericSetter, &dom_global_setterinfo), + JS_PS_END}; + +static const JSFunctionSpec dom_methods[] = { + JS_FNINFO("doFoo", dom_genericMethod, &doFoo_methodinfo, 3, + JSPROP_ENUMERATE), + JS_FS_END}; + +static const JSClass dom_class = { + "FakeDOMObject", JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(2)}; + +static const JSClass* GetDomClass() { return &dom_class; } + +static bool dom_genericGetter(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject()) { + args.rval().setUndefined(); + return true; + } + + RootedObject obj(cx, &args.thisv().toObject()); + if (JS::GetClass(obj) != &dom_class) { + args.rval().set(UndefinedValue()); + return true; + } + + JS::Value val = JS::GetReservedSlot(obj, DOM_OBJECT_SLOT); + + const JSJitInfo* info = FUNCTION_VALUE_TO_JITINFO(args.calleev()); + MOZ_ASSERT(info->type() == JSJitInfo::Getter); + JSJitGetterOp getter = info->getter; + return getter(cx, obj, val.toPrivate(), JSJitGetterCallArgs(args)); +} + +static bool dom_genericSetter(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1 || !args.thisv().isObject()) { + args.rval().setUndefined(); + return true; + } + + RootedObject obj(cx, &args.thisv().toObject()); + if (JS::GetClass(obj) != &dom_class) { + args.rval().set(UndefinedValue()); + return true; + } + + JS::Value val = JS::GetReservedSlot(obj, DOM_OBJECT_SLOT); + + const JSJitInfo* info = FUNCTION_VALUE_TO_JITINFO(args.calleev()); + MOZ_ASSERT(info->type() == JSJitInfo::Setter); + JSJitSetterOp setter = info->setter; + if (!setter(cx, obj, val.toPrivate(), JSJitSetterCallArgs(args))) { + return false; + } + args.rval().set(UndefinedValue()); + return true; +} + +static bool dom_genericMethod(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject()) { + args.rval().setUndefined(); + return true; + } + + RootedObject obj(cx, &args.thisv().toObject()); + if (JS::GetClass(obj) != &dom_class) { + args.rval().set(UndefinedValue()); + return true; + } + + JS::Value val = JS::GetReservedSlot(obj, DOM_OBJECT_SLOT); + + const JSJitInfo* info = FUNCTION_VALUE_TO_JITINFO(args.calleev()); + MOZ_ASSERT(info->type() == JSJitInfo::Method); + JSJitMethodOp method = info->method; + return method(cx, obj, val.toPrivate(), JSJitMethodCallArgs(args)); +} + +static void InitDOMObject(HandleObject obj) { + JS::SetReservedSlot(obj, DOM_OBJECT_SLOT, + PrivateValue(const_cast<void*>(DOM_PRIVATE_VALUE))); + JS::SetReservedSlot(obj, DOM_OBJECT_SLOT2, Int32Value(42)); +} + +static JSObject* GetDOMPrototype(JSContext* cx, JSObject* global) { + MOZ_ASSERT(JS_IsGlobalObject(global)); + if (JS::GetClass(global) != &global_class) { + JS_ReportErrorASCII(cx, "Can't get FakeDOMObject prototype in sandbox"); + return nullptr; + } + + const JS::Value& slot = JS::GetReservedSlot(global, DOM_PROTOTYPE_SLOT); + MOZ_ASSERT(slot.isObject()); + return &slot.toObject(); +} + +static bool dom_constructor(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject callee(cx, &args.callee()); + RootedValue protov(cx); + if (!GetProperty(cx, callee, callee, cx->names().prototype, &protov)) { + return false; + } + + if (!protov.isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_PROTOTYPE, + "FakeDOMObject"); + return false; + } + + RootedObject proto(cx, &protov.toObject()); + RootedObject domObj(cx, JS_NewObjectWithGivenProto(cx, &dom_class, proto)); + if (!domObj) { + return false; + } + + InitDOMObject(domObj); + + args.rval().setObject(*domObj); + return true; +} + +static bool InstanceClassHasProtoAtDepth(const JSClass* clasp, uint32_t protoID, + uint32_t depth) { + // Only the (fake) DOM object supports any JIT optimizations. + return clasp == GetDomClass(); +} + +static bool ShellBuildId(JS::BuildIdCharVector* buildId) { + // The browser embeds the date into the buildid and the buildid is embedded + // in the binary, so every 'make' necessarily builds a new firefox binary. + // Fortunately, the actual firefox executable is tiny -- all the code is in + // libxul.so and other shared modules -- so this isn't a big deal. Not so + // for the statically-linked JS shell. To avoid recompiling js.cpp and + // re-linking 'js' on every 'make', we use a constant buildid and rely on + // the shell user to manually clear any caches between cache-breaking updates. + const char buildid[] = "JS-shell"; + return buildId->append(buildid, sizeof(buildid)); +} + +static bool TimesAccessed(JSContext* cx, unsigned argc, Value* vp) { + static int32_t accessed = 0; + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setInt32(++accessed); + return true; +} + +static const JSPropertySpec TestingProperties[] = { + JS_PSG("timesAccessed", TimesAccessed, 0), JS_PS_END}; + +static JSObject* NewGlobalObject(JSContext* cx, JS::RealmOptions& options, + JSPrincipals* principals, ShellGlobalKind kind, + bool immutablePrototype) { + RootedObject glob(cx, + JS_NewGlobalObject(cx, &global_class, principals, + JS::DontFireOnNewGlobalHook, options)); + if (!glob) { + return nullptr; + } + + { + JSAutoRealm ar(cx, glob); + + if (kind == ShellGlobalKind::WindowProxy) { + RootedObject proxy(cx, NewShellWindowProxy(cx, glob)); + if (!proxy) { + return nullptr; + } + js::SetWindowProxy(cx, glob, proxy); + } + +#ifndef LAZY_STANDARD_CLASSES + if (!JS::InitRealmStandardClasses(cx)) { + return nullptr; + } +#endif + + if (immutablePrototype) { + bool succeeded; + if (!JS_SetImmutablePrototype(cx, glob, &succeeded)) { + return nullptr; + } + MOZ_ASSERT(succeeded, + "a fresh, unexposed global object is always capable of " + "having its [[Prototype]] be immutable"); + } + +#ifdef JS_HAS_CTYPES + if (!fuzzingSafe && !JS::InitCTypesClass(cx, glob)) { + return nullptr; + } +#endif + if (!JS_InitReflectParse(cx, glob)) { + return nullptr; + } + if (!JS_DefineDebuggerObject(cx, glob)) { + return nullptr; + } + if (!JS_DefineFunctionsWithHelp(cx, glob, shell_functions) || + !JS_DefineProfilingFunctions(cx, glob)) { + return nullptr; + } +#ifdef FUZZING_JS_FUZZILLI + if (!JS_DefineFunctions(cx, glob, shell_function_fuzzilli_hash)) { + return nullptr; + } +#endif + if (!js::DefineTestingFunctions(cx, glob, fuzzingSafe, + disableOOMFunctions)) { + return nullptr; + } + if (!JS_DefineProperties(cx, glob, TestingProperties)) { + return nullptr; + } + + if (!fuzzingSafe) { + if (!JS_DefineFunctionsWithHelp(cx, glob, fuzzing_unsafe_functions)) { + return nullptr; + } + if (!DefineConsole(cx, glob)) { + return nullptr; + } + } + + if (!DefineOS(cx, glob, fuzzingSafe, &gOutFile, &gErrFile)) { + return nullptr; + } + + if (!js::SupportDifferentialTesting()) { + if (!JS_DefineFunctionsWithHelp(cx, glob, + diff_testing_unsafe_functions)) { + return nullptr; + } + + RootedObject performanceObj(cx, JS_NewObject(cx, nullptr)); + if (!performanceObj) { + return nullptr; + } + if (!JS_DefineFunctionsWithHelp(cx, performanceObj, + performance_functions)) { + return nullptr; + } + RootedObject mozMemoryObj(cx, JS_NewObject(cx, nullptr)); + if (!mozMemoryObj) { + return nullptr; + } + RootedObject gcObj(cx, gc::NewMemoryInfoObject(cx)); + if (!gcObj) { + return nullptr; + } + if (!JS_DefineProperty(cx, glob, "performance", performanceObj, + JSPROP_ENUMERATE)) { + return nullptr; + } + if (!JS_DefineProperty(cx, performanceObj, "mozMemory", mozMemoryObj, + JSPROP_ENUMERATE)) { + return nullptr; + } + if (!JS_DefineProperty(cx, mozMemoryObj, "gc", gcObj, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + /* Initialize FakeDOMObject. */ + static const js::DOMCallbacks DOMcallbacks = {InstanceClassHasProtoAtDepth}; + SetDOMCallbacks(cx, &DOMcallbacks); + + RootedObject domProto( + cx, JS_InitClass(cx, glob, &dom_class, nullptr, "FakeDOMObject", + dom_constructor, 0, dom_props, dom_methods, nullptr, + nullptr)); + if (!domProto) { + return nullptr; + } + + // FakeDOMObject.prototype is the only DOM object which needs to retrieved + // in the shell; store it directly instead of creating a separate layer + // (ProtoAndIfaceCache) as done in the browser. + JS::SetReservedSlot(glob, DOM_PROTOTYPE_SLOT, ObjectValue(*domProto)); + + /* Initialize FakeDOMObject.prototype */ + InitDOMObject(domProto); + + if (!DefineToStringTag(cx, glob, cx->names().global)) { + return nullptr; + } + + JS_FireOnNewGlobalObject(cx, glob); + } + + return glob; +} + +static bool BindScriptArgs(JSContext* cx, OptionParser* op) { + AutoReportException are(cx); + + MultiStringRange msr = op->getMultiStringArg("scriptArgs"); + RootedObject scriptArgs(cx); + scriptArgs = JS::NewArrayObject(cx, 0); + if (!scriptArgs) { + return false; + } + + if (!JS_DefineProperty(cx, cx->global(), "scriptArgs", scriptArgs, 0)) { + return false; + } + + for (size_t i = 0; !msr.empty(); msr.popFront(), ++i) { + const char* scriptArg = msr.front(); + UniqueChars scriptArgUtf8 = JS::EncodeNarrowToUtf8(cx, scriptArg); + if (!scriptArgUtf8) { + return false; + } + RootedString str(cx, NewStringCopyUTF8(cx, scriptArgUtf8.get())); + if (!str || !JS_DefineElement(cx, scriptArgs, i, str, JSPROP_ENUMERATE)) { + return false; + } + } + + RootedValue scriptPathValue(cx); + if (const char* scriptPath = op->getStringArg("script")) { + UniqueChars scriptPathUtf8 = JS::EncodeNarrowToUtf8(cx, scriptPath); + if (!scriptPathUtf8) { + return false; + } + RootedString scriptPathString(cx, + NewStringCopyUTF8(cx, scriptPathUtf8.get())); + if (!scriptPathString) { + return false; + } + scriptPathValue = StringValue(scriptPathString); + } else { + scriptPathValue = UndefinedValue(); + } + + if (!JS_DefineProperty(cx, cx->global(), "scriptPath", scriptPathValue, 0)) { + return false; + } + + return true; +} + +static bool OptionFailure(const char* option, const char* str) { + fprintf(stderr, "Unrecognized option for %s: %s\n", option, str); + return false; +} + +template <typename... Ts> +auto minVal(Ts... args); +template <typename T> +auto minVal(T a) { + return a; +} + +template <typename T, typename... Ts> +auto minVal(T a, Ts... args) { + return std::min(a, minVal(args...)); +} + +[[nodiscard]] static bool ProcessArgs(JSContext* cx, OptionParser* op) { + ShellContext* sc = GetShellContext(cx); + + /* |scriptArgs| gets bound on the global before any code is run. */ + if (!BindScriptArgs(cx, op)) { + return false; + } + + MultiStringRange filePaths = op->getMultiStringOption('f'); + MultiStringRange utf16FilePaths = op->getMultiStringOption('u'); + MultiStringRange preludePaths = op->getMultiStringOption('p'); + MultiStringRange codeChunks = op->getMultiStringOption('e'); + MultiStringRange modulePaths = op->getMultiStringOption('m'); + +#ifdef FUZZING_JS_FUZZILLI + // Check for REPRL file source + if (op->getBoolOption("reprl")) { + return FuzzilliReprlGetAndRun(cx); + } +#endif /* FUZZING_JS_FUZZILLI */ + + if (filePaths.empty() && utf16FilePaths.empty() && codeChunks.empty() && + modulePaths.empty() && !op->getStringArg("script")) { + // Always use the interactive shell when -i is used. Without -i we let + // Process figure it out based on isatty. + bool forceTTY = op->getBoolOption('i'); + return Process(cx, nullptr, forceTTY, FileScript); + } + + while (!preludePaths.empty() || !filePaths.empty() || + !utf16FilePaths.empty() || !codeChunks.empty() || + !modulePaths.empty()) { + size_t ppArgno = preludePaths.empty() ? SIZE_MAX : preludePaths.argno(); + size_t fpArgno = filePaths.empty() ? SIZE_MAX : filePaths.argno(); + size_t ufpArgno = + utf16FilePaths.empty() ? SIZE_MAX : utf16FilePaths.argno(); + size_t ccArgno = codeChunks.empty() ? SIZE_MAX : codeChunks.argno(); + size_t mpArgno = modulePaths.empty() ? SIZE_MAX : modulePaths.argno(); + size_t minArgno = minVal(ppArgno, fpArgno, ufpArgno, ccArgno, mpArgno); + + if (ppArgno == minArgno) { + UniqueChars path = JS::EncodeNarrowToUtf8(cx, preludePaths.front()); + if (!path) { + return false; + } + if (!Process(cx, path.get(), false, PreludeScript)) { + return false; + } + + preludePaths.popFront(); + continue; + } + + if (fpArgno == minArgno) { + UniqueChars path = JS::EncodeNarrowToUtf8(cx, filePaths.front()); + if (!path) { + return false; + } + if (!Process(cx, path.get(), false, FileScript)) { + return false; + } + + filePaths.popFront(); + continue; + } + + if (ufpArgno == minArgno) { + UniqueChars path = JS::EncodeNarrowToUtf8(cx, utf16FilePaths.front()); + if (!path) { + return false; + } + if (!Process(cx, path.get(), false, FileScriptUtf16)) { + return false; + } + + utf16FilePaths.popFront(); + continue; + } + + if (ccArgno == minArgno) { + UniqueChars code = JS::EncodeNarrowToUtf8(cx, codeChunks.front()); + if (!code) { + return false; + } + + // Command line scripts are always parsed with full-parse to evaluate + // conditions which might filter code coverage conditions. + JS::CompileOptions opts(cx); + opts.setFileAndLine("-e", 1).setForceFullParse(); + + JS::SourceText<Utf8Unit> srcBuf; + if (!srcBuf.init(cx, code.get(), strlen(code.get()), + JS::SourceOwnership::Borrowed)) { + return false; + } + + RootedValue rval(cx); + if (!JS::Evaluate(cx, opts, srcBuf, &rval)) { + return false; + } + + codeChunks.popFront(); + if (sc->quitting) { + break; + } + + continue; + } + + MOZ_ASSERT(mpArgno == minArgno); + + UniqueChars path = JS::EncodeNarrowToUtf8(cx, modulePaths.front()); + if (!path) { + return false; + } + if (!Process(cx, path.get(), false, FileModule)) { + return false; + } + + modulePaths.popFront(); + } + + if (sc->quitting) { + return false; + } + + /* The |script| argument is processed after all options. */ + if (const char* path = op->getStringArg("script")) { + UniqueChars pathUtf8 = JS::EncodeNarrowToUtf8(cx, path); + if (!pathUtf8) { + return false; + } + if (!Process(cx, pathUtf8.get(), false, FileScript)) { + return false; + } + } + + if (op->getBoolOption('i')) { + if (!Process(cx, nullptr, true, FileScript)) { + return false; + } + } + + return true; +} + +static void SetWorkerContextOptions(JSContext* cx) { + // Copy option values from the main thread. + JS::ContextOptionsRef(cx) + .setAsmJS(enableAsmJS) + .setWasm(enableWasm) + .setWasmBaseline(enableWasmBaseline) + .setWasmIon(enableWasmOptimizing) +#define WASM_FEATURE(NAME, ...) .setWasm##NAME(enableWasm##NAME) + JS_FOR_WASM_FEATURES(WASM_FEATURE, WASM_FEATURE, WASM_FEATURE) +#undef WASM_FEATURE + + .setWasmVerbose(enableWasmVerbose) + .setTestWasmAwaitTier2(enableTestWasmAwaitTier2) + .setSourcePragmas(enableSourcePragmas); + + cx->runtime()->setOffthreadIonCompilationEnabled(offthreadCompilation); + cx->runtime()->profilingScripts = + enableCodeCoverage || enableDisassemblyDumps; + +#ifdef JS_GC_ZEAL + if (gZealBits && gZealFrequency) { + for (size_t i = 0; i < size_t(gc::ZealMode::Count); i++) { + if (gZealBits & (1 << i)) { + cx->runtime()->gc.setZeal(i, gZealFrequency); + } + } + } +#endif + + JS_SetNativeStackQuota(cx, gWorkerStackSize); +} + +[[nodiscard]] static bool PrintUnhandledRejection( + JSContext* cx, Handle<PromiseObject*> promise) { + RootedValue reason(cx, promise->reason()); + RootedObject site(cx, promise->resolutionSite()); + + RootedString str(cx, JS_ValueToSource(cx, reason)); + if (!str) { + return false; + } + + UniqueChars utf8chars = JS_EncodeStringToUTF8(cx, str); + if (!utf8chars) { + return false; + } + + FILE* fp = ErrorFilePointer(); + fprintf(fp, "Unhandled rejection: %s\n", utf8chars.get()); + + if (!site) { + fputs("(no stack trace available)\n", stderr); + return true; + } + + JSPrincipals* principals = cx->realm()->principals(); + RootedString stackStr(cx); + if (!BuildStackString(cx, principals, site, &stackStr, 2)) { + return false; + } + + UniqueChars stack = JS_EncodeStringToUTF8(cx, stackStr); + if (!stack) { + return false; + } + + fputs("Stack:\n", fp); + fputs(stack.get(), fp); + + return true; +} + +[[nodiscard]] static bool ReportUnhandledRejections(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + if (!sc->trackUnhandledRejections) { + return true; + } + + if (!sc->unhandledRejectedPromises) { + return true; + } + + AutoRealm ar(cx, sc->unhandledRejectedPromises); + + if (!SetObject::size(cx, sc->unhandledRejectedPromises)) { + return true; + } + + sc->exitCode = EXITCODE_RUNTIME_ERROR; + + RootedValue iter(cx); + if (!SetObject::iterator(cx, SetObject::IteratorKind::Values, + sc->unhandledRejectedPromises, &iter)) { + return false; + } + + Rooted<SetIteratorObject*> iterObj(cx, + &iter.toObject().as<SetIteratorObject>()); + JSObject* obj = SetIteratorObject::createResult(cx); + if (!obj) { + return false; + } + + Rooted<ArrayObject*> resultObj(cx, &obj->as<ArrayObject>()); + while (true) { + bool done = SetIteratorObject::next(iterObj, resultObj); + if (done) { + break; + } + + RootedObject obj(cx, &resultObj->getDenseElement(0).toObject()); + Rooted<PromiseObject*> promise(cx, obj->maybeUnwrapIf<PromiseObject>()); + if (!promise) { + FILE* fp = ErrorFilePointer(); + fputs( + "Unhandled rejection: dead proxy found in unhandled " + "rejections set\n", + fp); + continue; + } + + AutoRealm ar2(cx, promise); + + if (!PrintUnhandledRejection(cx, promise)) { + return false; + } + } + + sc->unhandledRejectedPromises = nullptr; + + return true; +} + +static int Shell(JSContext* cx, OptionParser* op) { +#ifdef JS_STRUCTURED_SPEW + cx->spewer().enableSpewing(); +#endif + + auto exitShell = MakeScopeExit([&] { +#ifdef JS_STRUCTURED_SPEW + cx->spewer().disableSpewing(); +#endif + }); + +#ifdef MOZ_CODE_COVERAGE + InstallCoverageSignalHandlers(); +#endif + + Maybe<JS::AutoDisableGenerationalGC> noggc; + if (op->getBoolOption("no-ggc")) { + noggc.emplace(cx); + } + + Maybe<AutoDisableCompactingGC> nocgc; + if (op->getBoolOption("no-cgc")) { + nocgc.emplace(cx); + } + + if (op->getBoolOption("fuzzing-safe")) { + fuzzingSafe = true; + } else { + fuzzingSafe = + (getenv("MOZ_FUZZING_SAFE") && getenv("MOZ_FUZZING_SAFE")[0] != '0'); + } + +#ifdef DEBUG + if (op->getBoolOption("differential-testing")) { + JS::SetSupportDifferentialTesting(true); + } +#endif + + if (op->getBoolOption("disable-oom-functions")) { + disableOOMFunctions = true; + } + + if (op->getBoolOption("more-compartments")) { + defaultToSameCompartment = false; + } + + bool reprl_mode = FuzzilliUseReprlMode(op); + + // Begin REPRL Loop + int result = EXIT_SUCCESS; + do { + JS::RealmOptions options; + SetStandardRealmOptions(options); + RootedObject glob( + cx, NewGlobalObject(cx, options, nullptr, ShellGlobalKind::WindowProxy, + /* immutablePrototype = */ true)); + if (!glob) { + return 1; + } + + JSAutoRealm ar(cx, glob); + + ShellContext* sc = GetShellContext(cx); + if (!sc->moduleLoader && !InitModuleLoader(cx, *op)) { + return EXIT_FAILURE; + } + +#ifdef FUZZING_INTERFACES + if (fuzzHaveModule) { + return FuzzJSRuntimeStart(cx, &sArgc, &sArgv); + } +#endif + + sc->exitCode = 0; + result = EXIT_SUCCESS; + { + AutoReportException are(cx); + if (!ProcessArgs(cx, op) && !sc->quitting) { + result = EXITCODE_RUNTIME_ERROR; + } + } + + /* + * The job queue must be drained even on error to finish outstanding async + * tasks before the main thread JSRuntime is torn down. Drain after + * uncaught exceptions have been reported since draining runs callbacks. + */ + RunShellJobs(cx); + + // Only if there's no other error, report unhandled rejections. + if (!result && !sc->exitCode) { + AutoReportException are(cx); + if (!ReportUnhandledRejections(cx)) { + FILE* fp = ErrorFilePointer(); + fputs("Error while printing unhandled rejection\n", fp); + } + } + + if (sc->exitCode) { + result = sc->exitCode; + } + +#ifdef FUZZING_JS_FUZZILLI + if (reprl_mode) { + fflush(stdout); + fflush(stderr); + // Send return code to parent and reset edge counters. + struct { + int status; + uint32_t execHash; + uint32_t execHashInputs; + } s; + s.status = (result & 0xff) << 8; + s.execHash = cx->executionHash; + s.execHashInputs = cx->executionHashInputs; + MOZ_RELEASE_ASSERT(write(REPRL_CWFD, &s, 12) == 12); + __sanitizer_cov_reset_edgeguards(); + cx->executionHash = 1; + cx->executionHashInputs = 0; + } +#endif + + if (enableDisassemblyDumps) { + AutoReportException are(cx); + if (!js::DumpRealmPCCounts(cx)) { + result = EXITCODE_OUT_OF_MEMORY; + } + } + + // End REPRL loop + } while (reprl_mode); + + return result; +} + +// Used to allocate memory when jemalloc isn't yet initialized. +JS_DECLARE_NEW_METHODS(SystemAlloc_New, malloc, static) + +static void SetOutputFile(const char* const envVar, RCFile* defaultOut, + RCFile** outFileP) { + RCFile* outFile; + + const char* outPath = getenv(envVar); + FILE* newfp; + if (outPath && *outPath && (newfp = fopen(outPath, "w"))) { + outFile = SystemAlloc_New<RCFile>(newfp); + } else { + outFile = defaultOut; + } + + if (!outFile) { + MOZ_CRASH("Failed to allocate output file"); + } + + outFile->acquire(); + *outFileP = outFile; +} + +static void PreInit() { +#ifdef XP_WIN + const char* crash_option = getenv("XRE_NO_WINDOWS_CRASH_DIALOG"); + if (crash_option && crash_option[0] == '1') { + // Disable the segfault dialog. We want to fail the tests immediately + // instead of hanging automation. + UINT newMode = SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX; + UINT prevMode = SetErrorMode(newMode); + SetErrorMode(prevMode | newMode); + } +#endif +} + +#ifndef JS_WITHOUT_NSPR +class AutoLibraryLoader { + Vector<PRLibrary*, 4, SystemAllocPolicy> libraries; + + public: + ~AutoLibraryLoader() { + for (auto dll : libraries) { + PR_UnloadLibrary(dll); + } + } + + PRLibrary* load(const char* path) { + PRLibSpec libSpec; + libSpec.type = PR_LibSpec_Pathname; + libSpec.value.pathname = path; + PRLibrary* dll = PR_LoadLibraryWithFlags(libSpec, PR_LD_NOW | PR_LD_GLOBAL); + if (!dll) { + fprintf(stderr, "LoadLibrary '%s' failed with code %d\n", path, + PR_GetError()); + MOZ_CRASH("Failed to load library"); + } + + MOZ_ALWAYS_TRUE(libraries.append(dll)); + return dll; + } +}; +#endif + +static bool ReadSelfHostedXDRFile(JSContext* cx, FileContents& buf) { + FILE* file = fopen(selfHostedXDRPath, "rb"); + if (!file) { + fprintf(stderr, "Can't open self-hosted stencil XDR file.\n"); + return false; + } + AutoCloseFile autoClose(file); + + struct stat st; + if (fstat(fileno(file), &st) < 0) { + fprintf(stderr, "Unable to stat self-hosted stencil XDR file.\n"); + return false; + } + + if (st.st_size >= INT32_MAX) { + fprintf(stderr, "self-hosted stencil XDR file too large.\n"); + return false; + } + uint32_t filesize = uint32_t(st.st_size); + + if (!buf.growBy(filesize)) { + return false; + } + size_t cc = fread(buf.begin(), 1, filesize, file); + if (cc != filesize) { + fprintf(stderr, "Short read on self-hosted stencil XDR file.\n"); + return false; + } + + return true; +} + +static bool WriteSelfHostedXDRFile(JSContext* cx, JS::SelfHostedCache buffer) { + FILE* file = fopen(selfHostedXDRPath, "wb"); + if (!file) { + JS_ReportErrorUTF8(cx, "Can't open self-hosted stencil XDR file."); + return false; + } + AutoCloseFile autoClose(file); + + size_t cc = fwrite(buffer.Elements(), 1, buffer.LengthBytes(), file); + if (cc != buffer.LengthBytes()) { + JS_ReportErrorUTF8(cx, "Short write on self-hosted stencil XDR file."); + return false; + } + + return true; +} + +static bool SetGCParameterFromArg(JSContext* cx, char* arg) { + char* c = strchr(arg, '='); + if (!c) { + fprintf(stderr, + "Error: --gc-param argument '%s' must be of the form " + "name=decimalValue\n", + arg); + return false; + } + + *c = '\0'; + const char* name = arg; + const char* valueStr = c + 1; + + JSGCParamKey key; + bool writable; + if (!GetGCParameterInfo(name, &key, &writable)) { + fprintf(stderr, "Error: Unknown GC parameter name '%s'\n", name); + fprintf(stderr, "Writable GC parameter names are:\n"); +#define PRINT_WRITABLE_PARAM_NAME(name, _, writable) \ + if (writable) { \ + fprintf(stderr, " %s\n", name); \ + } + FOR_EACH_GC_PARAM(PRINT_WRITABLE_PARAM_NAME) +#undef PRINT_WRITABLE_PARAM_NAME + return false; + } + + if (!writable) { + fprintf(stderr, "Error: GC parameter '%s' is not writable\n", name); + return false; + } + + char* end = nullptr; + unsigned long int value = strtoul(valueStr, &end, 10); + if (end == valueStr || *end) { + fprintf(stderr, + "Error: Could not parse '%s' as decimal for GC parameter '%s'\n", + valueStr, name); + return false; + } + + uint32_t paramValue = uint32_t(value); + if (value == ULONG_MAX || value != paramValue || + !cx->runtime()->gc.setParameter(cx, key, paramValue)) { + fprintf(stderr, "Error: Value %s is out of range for GC parameter '%s'\n", + valueStr, name); + return false; + } + + return true; +} + +int main(int argc, char** argv) { + PreInit(); + + sArgc = argc; + sArgv = argv; + + int result; + + setlocale(LC_ALL, ""); + + // Special-case stdout and stderr. We bump their refcounts to prevent them + // from getting closed and then having some printf fail somewhere. + RCFile rcStdout(stdout); + rcStdout.acquire(); + RCFile rcStderr(stderr); + rcStderr.acquire(); + + SetOutputFile("JS_STDOUT", &rcStdout, &gOutFile); + SetOutputFile("JS_STDERR", &rcStderr, &gErrFile); + + // Use a larger jemalloc page cache. This should match the value for browser + // foreground processes in ContentChild::RecvNotifyProcessPriorityChanged. + moz_set_max_dirty_page_modifier(4); + + OptionParser op("Usage: {progname} [options] [[script] scriptArgs*]"); + if (!InitOptionParser(op)) { + return EXIT_FAILURE; + } + + switch (op.parseArgs(argc, argv)) { + case OptionParser::EarlyExit: + return EXIT_SUCCESS; + case OptionParser::ParseError: + op.printHelp(argv[0]); + return EXIT_FAILURE; + case OptionParser::Fail: + return EXIT_FAILURE; + case OptionParser::Okay: + break; + } + + if (op.getHelpOption()) { + return EXIT_SUCCESS; + } + + if (!SetGlobalOptionsPreJSInit(op)) { + return EXIT_FAILURE; + } + + // Start the engine. + if (const char* message = JS_InitWithFailureDiagnostic()) { + fprintf(gErrFile->fp, "JS_Init failed: %s\n", message); + return 1; + } + + // `selfHostedXDRBuffer` contains XDR buffer of the self-hosted JS. + // A part of it is borrowed by ImmutableScriptData of the self-hosted scripts. + // + // This buffer should outlive JS_Shutdown. + Maybe<FileContents> selfHostedXDRBuffer; + + auto shutdownEngine = MakeScopeExit([] { JS_ShutDown(); }); + + if (!SetGlobalOptionsPostJSInit(op)) { + return EXIT_FAILURE; + } + + // Record aggregated telemetry data on disk. Do this as early as possible such + // that the telemetry is recording both before starting the context and after + // closing it. + auto writeTelemetryResults = MakeScopeExit([&op] { + if (telemetryLock) { + const char* dir = op.getStringOption("telemetry-dir"); + WriteTelemetryDataToDisk(dir); + js_free(telemetryLock); + telemetryLock = nullptr; + } + }); + + if (!InitSharedObjectMailbox()) { + return EXIT_FAILURE; + } + + JS::SetProcessBuildIdOp(ShellBuildId); + + /* Use the same parameters as the browser in xpcjsruntime.cpp. */ + JSContext* const cx = JS_NewContext(JS::DefaultHeapMaxBytes); + if (!cx) { + return 1; + } + + // Register telemetry callbacks, if needed. + if (telemetryLock) { + JS_SetAccumulateTelemetryCallback(cx, AccumulateTelemetryDataCallback); + } + + auto destroyCx = MakeScopeExit([cx] { JS_DestroyContext(cx); }); + + UniquePtr<ShellContext> sc = MakeUnique<ShellContext>(cx); + if (!sc) { + return 1; + } + auto destroyShellContext = MakeScopeExit([cx, &sc] { + // Must clear out some of sc's pointer containers before JS_DestroyContext. + sc->markObservers.reset(); + + JS_SetContextPrivate(cx, nullptr); + sc.reset(); + }); + + JS_SetContextPrivate(cx, sc.get()); + JS_AddExtraGCRootsTracer(cx, TraceBlackRoots, nullptr); + JS_SetGrayGCRootsTracer(cx, TraceGrayRoots, nullptr); + auto resetGrayGCRootsTracer = + MakeScopeExit([cx] { JS_SetGrayGCRootsTracer(cx, nullptr, nullptr); }); + + // Waiting is allowed on the shell's main thread, for now. + JS_SetFutexCanWait(cx); + JS::SetWarningReporter(cx, WarningReporter); + + if (!SetContextOptions(cx, op)) { + return 1; + } + + JS_SetTrustedPrincipals(cx, &ShellPrincipals::fullyTrusted); + JS_SetSecurityCallbacks(cx, &ShellPrincipals::securityCallbacks); + JS_InitDestroyPrincipalsCallback(cx, ShellPrincipals::destroy); + JS_SetDestroyCompartmentCallback(cx, DestroyShellCompartmentPrivate); + + js::SetWindowProxyClass(cx, &ShellWindowProxyClass); + + JS_AddInterruptCallback(cx, ShellInterruptCallback); + + JS::SetGCSliceCallback(cx, GCSliceCallback); + + bufferStreamState = js_new<ExclusiveWaitableData<BufferStreamState>>( + mutexid::BufferStreamState); + if (!bufferStreamState) { + return 1; + } + auto shutdownBufferStreams = MakeScopeExit([] { + ShutdownBufferStreams(); + js_delete(bufferStreamState); + }); + JS::InitConsumeStreamCallback(cx, ConsumeBufferSource, ReportStreamError); + + JS::SetPromiseRejectionTrackerCallback( + cx, ForwardingPromiseRejectionTrackerCallback); + + JS::dbg::SetDebuggerMallocSizeOf(cx, moz_malloc_size_of); + + js::UseInternalJobQueues(cx); + + JS::SetHostCleanupFinalizationRegistryCallback( + cx, ShellCleanupFinalizationRegistryCallback, sc.get()); + + auto shutdownShellThreads = MakeScopeExit([cx] { + KillWatchdog(cx); + KillWorkerThreads(cx); + DestructSharedObjectMailbox(); + CancelOffThreadJobsForRuntime(cx); + }); + + // The file content should stay alive as long as Worker thread can be + // initialized. + JS::SelfHostedCache xdrSpan = nullptr; + JS::SelfHostedWriter xdrWriter = nullptr; + if (selfHostedXDRPath) { + if (encodeSelfHostedCode) { + xdrWriter = WriteSelfHostedXDRFile; + } else { + selfHostedXDRBuffer.emplace(cx); + if (ReadSelfHostedXDRFile(cx, *selfHostedXDRBuffer)) { + MOZ_ASSERT(selfHostedXDRBuffer->length() > 0); + JS::SelfHostedCache span(selfHostedXDRBuffer->begin(), + selfHostedXDRBuffer->end()); + xdrSpan = span; + } else { + fprintf(stderr, "Falling back on parsing source.\n"); + selfHostedXDRPath = nullptr; + } + } + } + + if (!JS::InitSelfHostedCode(cx, xdrSpan, xdrWriter)) { + return 1; + } + + EnvironmentPreparer environmentPreparer(cx); + + JS::SetProcessLargeAllocationFailureCallback(my_LargeAllocFailCallback); + + js::SetPreserveWrapperCallbacks(cx, DummyPreserveWrapperCallback, + DummyHasReleasedWrapperCallback); + + if (op.getBoolOption("wasm-compile-and-serialize")) { +#ifdef __wasi__ + MOZ_CRASH("WASI doesn't support wasm"); +#else + if (!WasmCompileAndSerialize(cx)) { + // Errors have been printed directly to stderr. + MOZ_ASSERT(!cx->isExceptionPending()); + return EXIT_FAILURE; + } +#endif + return EXIT_SUCCESS; + } + + result = Shell(cx, &op); + +#ifdef DEBUG + if (OOM_printAllocationCount) { + printf("OOM max count: %" PRIu64 "\n", js::oom::simulator.counter()); + } +#endif + + return result; +} + +bool InitOptionParser(OptionParser& op) { + op.setDescription( + "The SpiderMonkey shell provides a command line interface to the " + "JavaScript engine. Code and file options provided via the command line " + "are " + "run left to right. If provided, the optional script argument is run " + "after " + "all options have been processed. Just-In-Time compilation modes may be " + "enabled via " + "command line options."); + op.setDescriptionWidth(72); + op.setHelpWidth(80); + op.setVersion(JS_GetImplementationVersion()); + + if (!op.addMultiStringOption( + 'f', "file", "PATH", + "File path to run, parsing file contents as UTF-8") || + !op.addMultiStringOption( + 'u', "utf16-file", "PATH", + "File path to run, inflating the file's UTF-8 contents to UTF-16 and " + "then parsing that") || + !op.addMultiStringOption('m', "module", "PATH", "Module path to run") || + !op.addMultiStringOption('p', "prelude", "PATH", "Prelude path to run") || + !op.addMultiStringOption('e', "execute", "CODE", "Inline code to run") || + !op.addStringOption('\0', "selfhosted-xdr-path", "[filename]", + "Read/Write selfhosted script data from/to the given " + "XDR file") || + !op.addStringOption('\0', "selfhosted-xdr-mode", "(encode,decode,off)", + "Whether to encode/decode data of the file provided" + "with --selfhosted-xdr-path.") || + !op.addBoolOption('i', "shell", "Enter prompt after running code") || + !op.addBoolOption('c', "compileonly", + "Only compile, don't run (syntax checking mode)") || + !op.addBoolOption('w', "warnings", "Emit warnings") || + !op.addBoolOption('W', "nowarnings", "Don't emit warnings") || + !op.addBoolOption('D', "dump-bytecode", + "Dump bytecode with exec count for all scripts") || + !op.addBoolOption('b', "print-timing", + "Print sub-ms runtime for each file that's run") || + !op.addBoolOption('\0', "code-coverage", + "Enable code coverage instrumentation.") || + !op.addBoolOption( + '\0', "disable-parser-deferred-alloc", + "Disable deferred allocation of GC objects until after parser") || +#ifdef DEBUG + !op.addBoolOption('O', "print-alloc", + "Print the number of allocations at exit") || +#endif + !op.addOptionalStringArg("script", + "A script to execute (after all options)") || + !op.addOptionalMultiStringArg( + "scriptArgs", + "String arguments to bind as |scriptArgs| in the " + "shell's global") || + !op.addIntOption( + '\0', "cpu-count", "COUNT", + "Set the number of CPUs (hardware threads) to COUNT, the " + "default is the actual number of CPUs. The total number of " + "background helper threads is the CPU count plus some constant.", + -1) || + !op.addIntOption('\0', "thread-count", "COUNT", "Alias for --cpu-count.", + -1) || + !op.addBoolOption('\0', "ion", "Enable IonMonkey (default)") || + !op.addBoolOption('\0', "no-ion", "Disable IonMonkey") || + !op.addBoolOption('\0', "no-ion-for-main-context", + "Disable IonMonkey for the main context only") || + !op.addIntOption('\0', "inlining-entry-threshold", "COUNT", + "The minimum stub entry count before trial-inlining a" + " call", + -1) || + !op.addIntOption('\0', "small-function-length", "COUNT", + "The maximum bytecode length of a 'small function' for " + "the purpose of inlining.", + -1) || + !op.addBoolOption('\0', "only-inline-selfhosted", + "Only inline selfhosted functions") || + !op.addBoolOption('\0', "no-asmjs", "Disable asm.js compilation") || + !op.addStringOption( + '\0', "wasm-compiler", "[option]", + "Choose to enable a subset of the wasm compilers, valid options are " + "'none', 'baseline', 'ion', 'optimizing', " + "'baseline+ion', 'baseline+optimizing'.") || + !op.addBoolOption('\0', "wasm-verbose", + "Enable WebAssembly verbose logging") || + !op.addBoolOption('\0', "disable-wasm-huge-memory", + "Disable WebAssembly huge memory") || + !op.addBoolOption('\0', "test-wasm-await-tier2", + "Forcibly activate tiering and block " + "instantiation on completion of tier2") || +#define WASM_DEFAULT_FEATURE(NAME, LOWER_NAME, COMPILE_PRED, COMPILER_PRED, \ + FLAG_PRED, SHELL, ...) \ + !op.addBoolOption('\0', "no-wasm-" SHELL, "Disable wasm " SHELL "feature.") || +#define WASM_TENTATIVE_FEATURE(NAME, LOWER_NAME, COMPILE_PRED, COMPILER_PRED, \ + FLAG_PRED, SHELL, ...) \ + !op.addBoolOption('\0', "no-wasm-" SHELL, \ + "Disable wasm " SHELL "feature.") || \ + !op.addBoolOption('\0', "wasm-" SHELL, "No-op.") || +#define WASM_EXPERIMENTAL_FEATURE(NAME, LOWER_NAME, COMPILE_PRED, \ + COMPILER_PRED, FLAG_PRED, SHELL, ...) \ + !op.addBoolOption('\0', "wasm-" SHELL, \ + "Enable experimental wasm " SHELL "feature.") || \ + !op.addBoolOption('\0', "no-wasm-" SHELL, "No-op.") || + JS_FOR_WASM_FEATURES(WASM_DEFAULT_FEATURE, WASM_TENTATIVE_FEATURE, + WASM_EXPERIMENTAL_FEATURE) +#undef WASM_DEFAULT_FEATURE +#undef WASM_TENTATIVE_FEATURE +#undef WASM_EXPERIMENTAL_FEATURE + !op.addBoolOption('\0', "no-native-regexp", + "Disable native regexp compilation") || + !op.addIntOption( + '\0', "regexp-warmup-threshold", "COUNT", + "Wait for COUNT invocations before compiling regexps to native code " + "(default 10)", + -1) || + !op.addBoolOption('\0', "trace-regexp-parser", "Trace regexp parsing") || + !op.addBoolOption('\0', "trace-regexp-assembler", + "Trace regexp assembler") || + !op.addBoolOption('\0', "trace-regexp-interpreter", + "Trace regexp interpreter") || + !op.addBoolOption('\0', "trace-regexp-peephole", + "Trace regexp peephole optimization") || + !op.addBoolOption('\0', "less-debug-code", + "Emit less machine code for " + "checking assertions under DEBUG.") || + !op.addBoolOption('\0', "disable-weak-refs", "Disable weak references") || + !op.addBoolOption('\0', "disable-tosource", "Disable toSource/uneval") || + !op.addBoolOption('\0', "disable-property-error-message-fix", + "Disable fix for the error message when accessing " + "property of null or undefined") || + !op.addBoolOption('\0', "enable-iterator-helpers", + "Enable iterator helpers") || + !op.addBoolOption('\0', "enable-shadow-realms", "Enable ShadowRealms") || + !op.addBoolOption('\0', "enable-array-grouping", + "Enable Array.grouping") || + !op.addBoolOption('\0', "enable-array-from-async", + "Enable Array.fromAsync") || + !op.addBoolOption('\0', "enable-well-formed-unicode-strings", + "Enable String.prototype.{is,to}WellFormed() methods" + "(Well-Formed Unicode Strings)") || + !op.addBoolOption('\0', "enable-change-array-by-copy", + "Enable change-array-by-copy methods") || + !op.addBoolOption('\0', "disable-change-array-by-copy", + "Disable change-array-by-copy methods") || +#ifdef ENABLE_NEW_SET_METHODS + !op.addBoolOption('\0', "enable-new-set-methods", + "Enable New Set methods") || + !op.addBoolOption('\0', "disable-new-set-methods", + "Disable New Set methods") || +#else + !op.addBoolOption('\0', "enable-new-set-methods", "no-op") || + !op.addBoolOption('\0', "disable-new-set-methods", "no-op") || +#endif + !op.addBoolOption('\0', "enable-top-level-await", + "Enable top-level await") || + !op.addBoolOption('\0', "enable-class-static-blocks", + "(no-op) Enable class static blocks") || + !op.addBoolOption('\0', "enable-import-assertions", + "Enable import assertions") || + !op.addStringOption('\0', "shared-memory", "on/off", + "SharedArrayBuffer and Atomics " +#if SHARED_MEMORY_DEFAULT + "(default: on, off to disable)" +#else + "(default: off, on to enable)" +#endif + ) || + !op.addStringOption('\0', "spectre-mitigations", "on/off", + "Whether Spectre mitigations are enabled (default: " + "off, on to enable)") || + !op.addStringOption('\0', "cache-ir-stubs", "on/off/call", + "Use CacheIR stubs (default: on, off to disable, " + "call to enable work-in-progress call ICs)") || + !op.addStringOption('\0', "ion-shared-stubs", "on/off", + "Use shared stubs (default: on, off to disable)") || + !op.addStringOption('\0', "ion-scalar-replacement", "on/off", + "Scalar Replacement (default: on, off to disable)") || + !op.addStringOption('\0', "ion-gvn", "[mode]", + "Specify Ion global value numbering:\n" + " off: disable GVN\n" + " on: enable GVN (default)\n") || + !op.addStringOption( + '\0', "ion-licm", "on/off", + "Loop invariant code motion (default: on, off to disable)") || + !op.addStringOption('\0', "ion-edgecase-analysis", "on/off", + "Find edge cases where Ion can avoid bailouts " + "(default: on, off to disable)") || + !op.addStringOption('\0', "ion-pruning", "on/off", + "Branch pruning (default: on, off to disable)") || + !op.addStringOption('\0', "ion-range-analysis", "on/off", + "Range analysis (default: on, off to disable)") || + !op.addStringOption('\0', "ion-sink", "on/off", + "Sink code motion (default: off, on to enable)") || + !op.addStringOption('\0', "ion-optimization-levels", "on/off", + "No-op for fuzzing") || + !op.addStringOption('\0', "ion-loop-unrolling", "on/off", + "(NOP for fuzzers)") || + !op.addStringOption( + '\0', "ion-instruction-reordering", "on/off", + "Instruction reordering (default: off, on to enable)") || + !op.addStringOption( + '\0', "ion-optimize-shapeguards", "on/off", + "Eliminate redundant shape guards (default: on, off to disable)") || + !op.addStringOption( + '\0', "ion-optimize-gcbarriers", "on/off", + "Eliminate redundant GC barriers (default: on, off to disable)") || + !op.addStringOption('\0', "ion-iterator-indices", "on/off", + "Optimize property access in for-in loops " + "(default: on, off to disable)") || + !op.addBoolOption('\0', "ion-check-range-analysis", + "Range analysis checking") || + !op.addBoolOption('\0', "ion-extra-checks", + "Perform extra dynamic validation checks") || + !op.addStringOption( + '\0', "ion-inlining", "on/off", + "Inline methods where possible (default: on, off to disable)") || + !op.addStringOption( + '\0', "ion-osr", "on/off", + "On-Stack Replacement (default: on, off to disable)") || + !op.addBoolOption('\0', "disable-bailout-loop-check", + "Turn off bailout loop check") || + !op.addBoolOption('\0', "enable-watchtower", + "Enable Watchtower optimizations") || + !op.addBoolOption('\0', "disable-watchtower", + "Disable Watchtower optimizations") || + !op.addBoolOption('\0', "enable-ic-frame-pointers", + "Use frame pointers in all IC stubs") || + !op.addBoolOption('\0', "scalar-replace-arguments", + "Use scalar replacement to optimize ArgumentsObject") || + !op.addStringOption( + '\0', "ion-limit-script-size", "on/off", + "Don't compile very large scripts (default: on, off to disable)") || + !op.addIntOption('\0', "ion-warmup-threshold", "COUNT", + "Wait for COUNT calls or iterations before compiling " + "at the normal optimization level (default: 1000)", + -1) || + !op.addIntOption('\0', "ion-full-warmup-threshold", "COUNT", + "No-op for fuzzing", -1) || + !op.addStringOption( + '\0', "ion-regalloc", "[mode]", + "Specify Ion register allocation:\n" + " backtracking: Priority based backtracking register allocation " + "(default)\n" + " testbed: Backtracking allocator with experimental features\n" + " stupid: Simple block local register allocation") || + !op.addBoolOption( + '\0', "ion-eager", + "Always ion-compile methods (implies --baseline-eager)") || + !op.addBoolOption('\0', "fast-warmup", + "Reduce warmup thresholds for each tier.") || + !op.addStringOption('\0', "ion-offthread-compile", "on/off", + "Compile scripts off thread (default: on)") || + !op.addStringOption('\0', "ion-parallel-compile", "on/off", + "--ion-parallel compile is deprecated. Use " + "--ion-offthread-compile.") || + !op.addBoolOption('\0', "baseline", + "Enable baseline compiler (default)") || + !op.addBoolOption('\0', "no-baseline", "Disable baseline compiler") || + !op.addBoolOption('\0', "baseline-eager", + "Always baseline-compile methods") || + !op.addIntOption( + '\0', "baseline-warmup-threshold", "COUNT", + "Wait for COUNT calls or iterations before baseline-compiling " + "(default: 10)", + -1) || + !op.addBoolOption('\0', "blinterp", + "Enable Baseline Interpreter (default)") || + !op.addBoolOption('\0', "no-blinterp", "Disable Baseline Interpreter") || + !op.addBoolOption('\0', "disable-jithints", + "Disable caching eager baseline compilation hints.") || + !op.addBoolOption( + '\0', "emit-interpreter-entry", + "Emit Interpreter entry trampolines (default under --enable-perf)") || + !op.addBoolOption( + '\0', "no-emit-interpreter-entry", + "Do not emit Interpreter entry trampolines (default).") || + !op.addBoolOption('\0', "blinterp-eager", + "Always Baseline-interpret scripts") || + !op.addIntOption( + '\0', "blinterp-warmup-threshold", "COUNT", + "Wait for COUNT calls or iterations before Baseline-interpreting " + "(default: 10)", + -1) || + !op.addIntOption( + '\0', "trial-inlining-warmup-threshold", "COUNT", + "Wait for COUNT calls or iterations before trial-inlining " + "(default: 500)", + -1) || + !op.addBoolOption( + '\0', "non-writable-jitcode", + "(NOP for fuzzers) Allocate JIT code as non-writable memory.") || + !op.addBoolOption( + '\0', "no-sse3", + "Pretend CPU does not support SSE3 instructions and above " + "to test JIT codegen (no-op on platforms other than x86 and x64).") || + !op.addBoolOption( + '\0', "no-ssse3", + "Pretend CPU does not support SSSE3 [sic] instructions and above " + "to test JIT codegen (no-op on platforms other than x86 and x64).") || + !op.addBoolOption( + '\0', "no-sse41", + "Pretend CPU does not support SSE4.1 instructions " + "to test JIT codegen (no-op on platforms other than x86 and x64).") || + !op.addBoolOption('\0', "no-sse4", "Alias for --no-sse41") || + !op.addBoolOption( + '\0', "no-sse42", + "Pretend CPU does not support SSE4.2 instructions " + "to test JIT codegen (no-op on platforms other than x86 and x64).") || +#ifdef ENABLE_WASM_AVX + !op.addBoolOption('\0', "enable-avx", + "No-op. AVX is enabled by default, if available.") || + !op.addBoolOption( + '\0', "no-avx", + "Pretend CPU does not support AVX or AVX2 instructions " + "to test JIT codegen (no-op on platforms other than x86 and x64).") || +#else + !op.addBoolOption('\0', "enable-avx", + "AVX is disabled by default. Enable AVX. " + "(no-op on platforms other than x86 and x64).") || + !op.addBoolOption('\0', "no-avx", + "No-op. AVX is currently disabled by default.") || +#endif + !op.addBoolOption('\0', "more-compartments", + "Make newGlobal default to creating a new " + "compartment.") || + !op.addBoolOption('\0', "fuzzing-safe", + "Don't expose functions that aren't safe for " + "fuzzers to call") || +#ifdef DEBUG + !op.addBoolOption('\0', "differential-testing", + "Avoid random/undefined behavior that disturbs " + "differential testing (correctness fuzzing)") || +#endif + !op.addBoolOption('\0', "disable-oom-functions", + "Disable functions that cause " + "artificial OOMs") || + !op.addBoolOption('\0', "no-threads", "Disable helper threads") || + !op.addBoolOption( + '\0', "no-jit-backend", + "Disable the JIT backend completely for this process") || +#ifdef DEBUG + !op.addBoolOption('\0', "dump-entrained-variables", + "Print variables which are " + "unnecessarily entrained by inner functions") || +#endif + !op.addBoolOption('\0', "no-ggc", "Disable Generational GC") || + !op.addBoolOption('\0', "no-cgc", "Disable Compacting GC") || + !op.addBoolOption('\0', "no-incremental-gc", "Disable Incremental GC") || + !op.addBoolOption('\0', "enable-parallel-marking", + "Turn on parallel marking") || + !op.addIntOption( + '\0', "marking-threads", "COUNT", + "Set the number of threads used for parallel marking to COUNT.", 0) || + !op.addStringOption('\0', "nursery-strings", "on/off", + "Allocate strings in the nursery") || + !op.addStringOption('\0', "nursery-bigints", "on/off", + "Allocate BigInts in the nursery") || + !op.addIntOption('\0', "available-memory", "SIZE", + "Select GC settings based on available memory (MB)", + 0) || + !op.addStringOption('\0', "arm-hwcap", "[features]", + "Specify ARM code generation features, or 'help' to " + "list all features.") || + !op.addIntOption('\0', "arm-asm-nop-fill", "SIZE", + "Insert the given number of NOP instructions at all " + "possible pool locations.", + 0) || + !op.addIntOption('\0', "asm-pool-max-offset", "OFFSET", + "The maximum pc relative OFFSET permitted in pool " + "reference instructions.", + 1024) || + !op.addBoolOption('\0', "arm-sim-icache-checks", + "Enable icache flush checks in the ARM " + "simulator.") || + !op.addIntOption('\0', "arm-sim-stop-at", "NUMBER", + "Stop the ARM simulator after the given " + "NUMBER of instructions.", + -1) || + !op.addBoolOption('\0', "mips-sim-icache-checks", + "Enable icache flush checks in the MIPS " + "simulator.") || + !op.addIntOption('\0', "mips-sim-stop-at", "NUMBER", + "Stop the MIPS simulator after the given " + "NUMBER of instructions.", + -1) || + !op.addBoolOption('\0', "loong64-sim-icache-checks", + "Enable icache flush checks in the LoongArch64 " + "simulator.") || + !op.addIntOption('\0', "loong64-sim-stop-at", "NUMBER", + "Stop the LoongArch64 simulator after the given " + "NUMBER of instructions.", + -1) || +#ifdef JS_CODEGEN_RISCV64 + !op.addBoolOption('\0', "riscv-debug", "debug print riscv info.") || +#endif +#ifdef JS_SIMULATOR_RISCV64 + !op.addBoolOption('\0', "trace-sim", "print simulator info.") || + !op.addBoolOption('\0', "debug-sim", "debug simulator.") || + !op.addBoolOption('\0', "riscv-trap-to-simulator-debugger", + "trap into simulator debuggger.") || + !op.addIntOption('\0', "riscv-sim-stop-at", "NUMBER", + "Stop the riscv simulator after the given " + "NUMBER of instructions.", + -1) || +#endif + !op.addIntOption('\0', "nursery-size", "SIZE-MB", + "Set the maximum nursery size in MB", + JS::DefaultNurseryMaxBytes / 1024 / 1024) || +#ifdef JS_GC_ZEAL + !op.addStringOption('z', "gc-zeal", "LEVEL(;LEVEL)*[,N]", + gc::ZealModeHelpText) || +#else + !op.addStringOption('z', "gc-zeal", "LEVEL(;LEVEL)*[,N]", + "option ignored in non-gc-zeal builds") || +#endif + !op.addMultiStringOption('\0', "gc-param", "NAME=VALUE", + "Set a named GC parameter") || + !op.addStringOption('\0', "module-load-path", "DIR", + "Set directory to load modules from") || + !op.addBoolOption('\0', "no-source-pragmas", + "Disable source(Mapping)URL pragma parsing") || + !op.addBoolOption('\0', "no-async-stacks", "Disable async stacks") || + !op.addBoolOption('\0', "async-stacks-capture-debuggee-only", + "Limit async stack capture to only debuggees") || + !op.addMultiStringOption('\0', "dll", "LIBRARY", + "Dynamically load LIBRARY") || + !op.addBoolOption('\0', "suppress-minidump", + "Suppress crash minidumps") || +#ifdef JS_ENABLE_SMOOSH + !op.addBoolOption('\0', "smoosh", "Use SmooshMonkey") || + !op.addStringOption('\0', "not-implemented-watchfile", "[filename]", + "Track NotImplemented errors in the new frontend") || +#else + !op.addBoolOption('\0', "smoosh", "No-op") || +#endif + !op.addStringOption( + '\0', "delazification-mode", "[option]", + "Select one of the delazification mode for scripts given on the " + "command line, valid options are: " + "'on-demand', 'concurrent-df', 'eager', 'concurrent-df+on-demand'. " + "Choosing 'concurrent-df+on-demand' will run both concurrent-df and " + "on-demand delazification mode, and compare compilation outcome. ") || + !op.addBoolOption('\0', "wasm-compile-and-serialize", + "Compile the wasm bytecode from stdin and serialize " + "the results to stdout") || +#ifdef FUZZING_JS_FUZZILLI + !op.addBoolOption('\0', "reprl", "Enable REPRL mode for fuzzing") || +#endif + !op.addStringOption('\0', "telemetry-dir", "[directory]", + "Output telemetry results in a directory") || + !op.addBoolOption('\0', "use-fdlibm-for-sin-cos-tan", + "Use fdlibm for Math.sin, Math.cos, and Math.tan")) { + return false; + } + + op.setArgTerminatesOptions("script", true); + op.setArgCapturesRest("scriptArgs"); + + return true; +} + +bool SetGlobalOptionsPreJSInit(const OptionParser& op) { + // Note: DisableJitBackend must be called before JS_InitWithFailureDiagnostic. + if (op.getBoolOption("no-jit-backend")) { + JS::DisableJitBackend(); + } + +#if defined(JS_CODEGEN_ARM) + if (const char* str = op.getStringOption("arm-hwcap")) { + jit::SetARMHwCapFlagsString(str); + } + + int32_t fill = op.getIntOption("arm-asm-nop-fill"); + if (fill >= 0) { + jit::Assembler::NopFill = fill; + } + + int32_t poolMaxOffset = op.getIntOption("asm-pool-max-offset"); + if (poolMaxOffset >= 5 && poolMaxOffset <= 1024) { + jit::Assembler::AsmPoolMaxOffset = poolMaxOffset; + } +#endif + + // Fish around in `op` for various important compiler-configuration flags + // and make sure they get handed on to any child processes we might create. + // See bug 1700900. Semantically speaking, this is all rather dubious: + // + // * What set of flags need to be propagated in order to guarantee that the + // child produces code that is "compatible" (in whatever sense) with that + // produced by the parent? This isn't always easy to determine. + // + // * There's nothing that ensures that flags given to the child are + // presented in the same order that they exist in the parent's `argv[]`. + // That could be a problem in the case where two flags with contradictory + // meanings are given, and they are presented to the child in the opposite + // order. For example: --wasm-compiler=optimizing --wasm-compiler=baseline. + +#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64) + MOZ_ASSERT(!js::jit::CPUFlagsHaveBeenComputed()); + + if (op.getBoolOption("no-sse3")) { + js::jit::CPUInfo::SetSSE3Disabled(); + if (!sCompilerProcessFlags.append("--no-sse3")) { + return false; + } + } + if (op.getBoolOption("no-ssse3")) { + js::jit::CPUInfo::SetSSSE3Disabled(); + if (!sCompilerProcessFlags.append("--no-ssse3")) { + return false; + } + } + if (op.getBoolOption("no-sse4") || op.getBoolOption("no-sse41")) { + js::jit::CPUInfo::SetSSE41Disabled(); + if (!sCompilerProcessFlags.append("--no-sse41")) { + return false; + } + } + if (op.getBoolOption("no-sse42")) { + js::jit::CPUInfo::SetSSE42Disabled(); + if (!sCompilerProcessFlags.append("--no-sse42")) { + return false; + } + } + if (op.getBoolOption("no-avx")) { + js::jit::CPUInfo::SetAVXDisabled(); + if (!sCompilerProcessFlags.append("--no-avx")) { + return false; + } + } + if (op.getBoolOption("enable-avx")) { + js::jit::CPUInfo::SetAVXEnabled(); + if (!sCompilerProcessFlags.append("--enable-avx")) { + return false; + } + } +#endif + + return true; +} + +bool SetGlobalOptionsPostJSInit(const OptionParser& op) { + if (op.getStringOption("telemetry-dir")) { + MOZ_ASSERT(!telemetryLock); + telemetryLock = js_new<Mutex>(mutexid::ShellTelemetry); + if (!telemetryLock) { + return false; + } + } + + // Allow dumping on Linux with the fuzzing flag set, even when running with + // the suid/sgid flag set on the shell. +#ifdef XP_LINUX + if (op.getBoolOption("fuzzing-safe")) { + prctl(PR_SET_DUMPABLE, 1); + } +#endif + +#ifdef DEBUG + /* + * Process OOM options as early as possible so that we can observe as many + * allocations as possible. + */ + OOM_printAllocationCount = op.getBoolOption('O'); +#endif + + if (op.getBoolOption("no-threads")) { + js::DisableExtraThreads(); + } + + enableCodeCoverage = op.getBoolOption("code-coverage"); + if (enableCodeCoverage) { + js::EnableCodeCoverage(); + } + + // If LCov is enabled, then the default delazification mode should be changed + // to parse everything eagerly, such that we know the location of every + // instruction, to report them in the LCov summary, even if there is no uses + // of these instructions. + // + // Note: code coverage can be enabled either using the --code-coverage command + // line, or the JS_CODE_COVERAGE_OUTPUT_DIR environment variable, which is + // processed by JS_InitWithFailureDiagnostic. + if (coverage::IsLCovEnabled()) { + defaultDelazificationMode = + JS::DelazificationOption::ParseEverythingEagerly; + } + + if (const char* xdr = op.getStringOption("selfhosted-xdr-path")) { + shell::selfHostedXDRPath = xdr; + } + if (const char* opt = op.getStringOption("selfhosted-xdr-mode")) { + if (strcmp(opt, "encode") == 0) { + shell::encodeSelfHostedCode = true; + } else if (strcmp(opt, "decode") == 0) { + shell::encodeSelfHostedCode = false; + } else if (strcmp(opt, "off") == 0) { + shell::selfHostedXDRPath = nullptr; + } else { + MOZ_CRASH( + "invalid option value for --selfhosted-xdr-mode, must be " + "encode/decode"); + } + } + +#ifdef JS_WITHOUT_NSPR + if (!op.getMultiStringOption("dll").empty()) { + fprintf(stderr, "Error: --dll requires NSPR support!\n"); + return false; + } +#else + AutoLibraryLoader loader; + MultiStringRange dllPaths = op.getMultiStringOption("dll"); + while (!dllPaths.empty()) { + char* path = dllPaths.front(); + loader.load(path); + dllPaths.popFront(); + } +#endif + + if (op.getBoolOption("suppress-minidump")) { + js::NoteIntentionalCrash(); + } + + // The fake CPU count must be set before initializing the Runtime, + // which spins up the thread pool. + int32_t cpuCount = op.getIntOption("cpu-count"); // What we're really setting + if (cpuCount < 0) { + cpuCount = op.getIntOption("thread-count"); // Legacy name + } + if (cpuCount >= 0 && !SetFakeCPUCount(cpuCount)) { + return false; + } + + return true; +} + +bool SetContextOptions(JSContext* cx, const OptionParser& op) { + if (!SetContextWasmOptions(cx, op) || !SetContextJITOptions(cx, op) || + !SetContextGCOptions(cx, op)) { + return false; + } + + enableSourcePragmas = !op.getBoolOption("no-source-pragmas"); + enableAsyncStacks = !op.getBoolOption("no-async-stacks"); + enableAsyncStackCaptureDebuggeeOnly = + op.getBoolOption("async-stacks-capture-debuggee-only"); + enableWeakRefs = !op.getBoolOption("disable-weak-refs"); + enableToSource = !op.getBoolOption("disable-tosource"); + enablePropertyErrorMessageFix = + !op.getBoolOption("disable-property-error-message-fix"); + enableIteratorHelpers = op.getBoolOption("enable-iterator-helpers"); + enableShadowRealms = op.getBoolOption("enable-shadow-realms"); + enableArrayFromAsync = op.getBoolOption("enable-array-from-async"); +#ifdef NIGHTLY_BUILD + enableArrayGrouping = op.getBoolOption("enable-array-grouping"); + enableWellFormedUnicodeStrings = + op.getBoolOption("enable-well-formed-unicode-strings"); +#endif + enableChangeArrayByCopy = !op.getBoolOption("disable-change-array-by-copy"); +#ifdef ENABLE_NEW_SET_METHODS + enableNewSetMethods = op.getBoolOption("enable-new-set-methods"); +#endif + enableImportAssertions = op.getBoolOption("enable-import-assertions"); + useFdlibmForSinCosTan = op.getBoolOption("use-fdlibm-for-sin-cos-tan"); + + JS::ContextOptionsRef(cx) + .setSourcePragmas(enableSourcePragmas) + .setAsyncStack(enableAsyncStacks) + .setAsyncStackCaptureDebuggeeOnly(enableAsyncStackCaptureDebuggeeOnly) + .setImportAssertions(enableImportAssertions); + + JS::SetUseFdlibmForSinCosTan(useFdlibmForSinCosTan); + + if (const char* str = op.getStringOption("shared-memory")) { + if (strcmp(str, "off") == 0) { + enableSharedMemory = false; + } else if (strcmp(str, "on") == 0) { + enableSharedMemory = true; + } else { + return OptionFailure("shared-memory", str); + } + } + + reportWarnings = op.getBoolOption('w'); + compileOnly = op.getBoolOption('c'); + printTiming = op.getBoolOption('b'); + enableDisassemblyDumps = op.getBoolOption('D'); + cx->runtime()->profilingScripts = + enableCodeCoverage || enableDisassemblyDumps; + +#ifdef JS_ENABLE_SMOOSH + if (op.getBoolOption("smoosh")) { + JS::ContextOptionsRef(cx).setTrySmoosh(true); + js::frontend::InitSmoosh(); + } + + if (const char* filename = op.getStringOption("not-implemented-watchfile")) { + FILE* out = fopen(filename, "a"); + MOZ_RELEASE_ASSERT(out); + setbuf(out, nullptr); // Make unbuffered + cx->runtime()->parserWatcherFile.init(out); + JS::ContextOptionsRef(cx).setTrackNotImplemented(true); + } +#endif + + if (const char* mode = op.getStringOption("delazification-mode")) { + if (strcmp(mode, "on-demand") == 0) { + defaultDelazificationMode = JS::DelazificationOption::OnDemandOnly; + } else if (strcmp(mode, "concurrent-df") == 0) { + defaultDelazificationMode = + JS::DelazificationOption::ConcurrentDepthFirst; + } else if (strcmp(mode, "eager") == 0) { + defaultDelazificationMode = + JS::DelazificationOption::ParseEverythingEagerly; + } else if (strcmp(mode, "concurrent-df+on-demand") == 0 || + strcmp(mode, "on-demand+concurrent-df") == 0) { + defaultDelazificationMode = + JS::DelazificationOption::CheckConcurrentWithOnDemand; + } else { + return OptionFailure("delazification-mode", mode); + } + } + + return true; +} + +bool SetContextWasmOptions(JSContext* cx, const OptionParser& op) { + enableAsmJS = !op.getBoolOption("no-asmjs"); + + enableWasm = true; + enableWasmBaseline = true; + enableWasmOptimizing = true; + + if (const char* str = op.getStringOption("wasm-compiler")) { + if (strcmp(str, "none") == 0) { + enableWasm = false; + } else if (strcmp(str, "baseline") == 0) { + MOZ_ASSERT(enableWasmBaseline); + enableWasmOptimizing = false; + } else if (strcmp(str, "optimizing") == 0 || + strcmp(str, "optimized") == 0) { + enableWasmBaseline = false; + MOZ_ASSERT(enableWasmOptimizing); + } else if (strcmp(str, "baseline+optimizing") == 0 || + strcmp(str, "baseline+optimized") == 0) { + MOZ_ASSERT(enableWasmBaseline); + MOZ_ASSERT(enableWasmOptimizing); + } else if (strcmp(str, "ion") == 0) { + enableWasmBaseline = false; + enableWasmOptimizing = true; + } else if (strcmp(str, "baseline+ion") == 0) { + MOZ_ASSERT(enableWasmBaseline); + enableWasmOptimizing = true; + } else { + return OptionFailure("wasm-compiler", str); + } + } + +#define WASM_DEFAULT_FEATURE(NAME, LOWER_NAME, COMPILE_PRED, COMPILER_PRED, \ + FLAG_PRED, SHELL, ...) \ + enableWasm##NAME = !op.getBoolOption("no-wasm-" SHELL); +#define WASM_EXPERIMENTAL_FEATURE(NAME, LOWER_NAME, COMPILE_PRED, \ + COMPILER_PRED, FLAG_PRED, SHELL, ...) \ + enableWasm##NAME = op.getBoolOption("wasm-" SHELL); + JS_FOR_WASM_FEATURES(WASM_DEFAULT_FEATURE, WASM_DEFAULT_FEATURE, + WASM_EXPERIMENTAL_FEATURE); +#undef WASM_DEFAULT_FEATURE +#undef WASM_EXPERIMENTAL_FEATURE + + enableWasmVerbose = op.getBoolOption("wasm-verbose"); + enableTestWasmAwaitTier2 = op.getBoolOption("test-wasm-await-tier2"); + + JS::ContextOptionsRef(cx) + .setAsmJS(enableAsmJS) + .setWasm(enableWasm) + .setWasmForTrustedPrinciples(enableWasm) + .setWasmBaseline(enableWasmBaseline) + .setWasmIon(enableWasmOptimizing) +#define WASM_FEATURE(NAME, ...) .setWasm##NAME(enableWasm##NAME) + JS_FOR_WASM_FEATURES(WASM_FEATURE, WASM_FEATURE, WASM_FEATURE) +#undef WASM_FEATURE + ; + +#ifndef __wasi__ + // This must be set before self-hosted code is initialized, as self-hosted + // code reads the property and the property may not be changed later. + bool disabledHugeMemory = false; + if (op.getBoolOption("disable-wasm-huge-memory")) { + disabledHugeMemory = JS::DisableWasmHugeMemory(); + MOZ_RELEASE_ASSERT(disabledHugeMemory); + } + + // --disable-wasm-huge-memory needs to be propagated. See bug 1518210. + if (disabledHugeMemory && + !sCompilerProcessFlags.append("--disable-wasm-huge-memory")) { + return false; + } + + // Also the following are to be propagated. + const char* to_propagate[] = { +# define WASM_DEFAULT_FEATURE(NAME, LOWER_NAME, COMPILE_PRED, COMPILER_PRED, \ + FLAG_PRED, SHELL, ...) \ + "--no-wasm-" SHELL, +# define WASM_EXPERIMENTAL_FEATURE(NAME, LOWER_NAME, COMPILE_PRED, \ + COMPILER_PRED, FLAG_PRED, SHELL, ...) \ + "--wasm-" SHELL, + JS_FOR_WASM_FEATURES(WASM_DEFAULT_FEATURE, WASM_DEFAULT_FEATURE, + WASM_EXPERIMENTAL_FEATURE) +# undef WASM_DEFAULT_FEATURE +# undef WASM_EXPERIMENTAL_FEATURE + // Compiler selection options + "--test-wasm-await-tier2", + NULL}; + for (const char** p = &to_propagate[0]; *p; p++) { + if (op.getBoolOption(&(*p)[2] /* 2 => skip the leading '--' */)) { + if (!sCompilerProcessFlags.append(*p)) { + return false; + } + } + } + + // Also --wasm-compiler= is to be propagated. This is tricky because it is + // necessary to reconstitute the --wasm-compiler=<whatever> string from its + // pieces, without causing a leak. Hence it is copied into a static buffer. + // This is thread-unsafe, but we're in `main()` and on the process' root + // thread. Also, we do this only once -- it wouldn't work properly if we + // handled multiple --wasm-compiler= flags in a loop. + const char* wasm_compiler = op.getStringOption("wasm-compiler"); + if (wasm_compiler) { + size_t n_needed = + 2 + strlen("wasm-compiler") + 1 + strlen(wasm_compiler) + 1; + const size_t n_avail = 128; + static char buf[n_avail]; + // `n_needed` depends on the compiler name specified. However, it can't + // be arbitrarily long, since previous flag-checking should have limited + // it to a set of known possibilities: "baseline", "ion", + // "baseline+ion", Still, assert this for safety. + MOZ_RELEASE_ASSERT(n_needed < n_avail); + memset(buf, 0, sizeof(buf)); + SprintfBuf(buf, n_avail, "--%s=%s", "wasm-compiler", wasm_compiler); + if (!sCompilerProcessFlags.append(buf)) { + return false; + } + } +#endif // __wasi__ + + return true; +} + +bool SetContextJITOptions(JSContext* cx, const OptionParser& op) { + // Check --fast-warmup first because it sets default warm-up thresholds. These + // thresholds can then be overridden below by --ion-eager and other flags. + if (op.getBoolOption("fast-warmup")) { + jit::JitOptions.setFastWarmUp(); + } + + if (op.getBoolOption("no-ion-for-main-context")) { + JS::ContextOptionsRef(cx).setDisableIon(); + } + + if (const char* str = op.getStringOption("cache-ir-stubs")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableCacheIR = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableCacheIR = true; + } else { + return OptionFailure("cache-ir-stubs", str); + } + } + + if (const char* str = op.getStringOption("spectre-mitigations")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.spectreIndexMasking = true; + jit::JitOptions.spectreObjectMitigations = true; + jit::JitOptions.spectreStringMitigations = true; + jit::JitOptions.spectreValueMasking = true; + jit::JitOptions.spectreJitToCxxCalls = true; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.spectreIndexMasking = false; + jit::JitOptions.spectreObjectMitigations = false; + jit::JitOptions.spectreStringMitigations = false; + jit::JitOptions.spectreValueMasking = false; + jit::JitOptions.spectreJitToCxxCalls = false; + } else { + return OptionFailure("spectre-mitigations", str); + } + } + + if (const char* str = op.getStringOption("ion-scalar-replacement")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableScalarReplacement = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableScalarReplacement = true; + } else { + return OptionFailure("ion-scalar-replacement", str); + } + } + + if (op.getStringOption("ion-shared-stubs")) { + // Dead option, preserved for now for potential fuzzer interaction. + } + + if (const char* str = op.getStringOption("ion-gvn")) { + if (strcmp(str, "off") == 0) { + jit::JitOptions.disableGvn = true; + } else if (strcmp(str, "on") != 0 && strcmp(str, "optimistic") != 0 && + strcmp(str, "pessimistic") != 0) { + // We accept "pessimistic" and "optimistic" as synonyms for "on" + // for backwards compatibility. + return OptionFailure("ion-gvn", str); + } + } + + if (const char* str = op.getStringOption("ion-licm")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableLicm = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableLicm = true; + } else { + return OptionFailure("ion-licm", str); + } + } + + if (const char* str = op.getStringOption("ion-edgecase-analysis")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableEdgeCaseAnalysis = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableEdgeCaseAnalysis = true; + } else { + return OptionFailure("ion-edgecase-analysis", str); + } + } + + if (const char* str = op.getStringOption("ion-pruning")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disablePruning = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disablePruning = true; + } else { + return OptionFailure("ion-pruning", str); + } + } + + if (const char* str = op.getStringOption("ion-range-analysis")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableRangeAnalysis = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableRangeAnalysis = true; + } else { + return OptionFailure("ion-range-analysis", str); + } + } + + if (const char* str = op.getStringOption("ion-sink")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableSink = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableSink = true; + } else { + return OptionFailure("ion-sink", str); + } + } + + if (const char* str = op.getStringOption("ion-optimize-shapeguards")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableRedundantShapeGuards = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableRedundantShapeGuards = true; + } else { + return OptionFailure("ion-optimize-shapeguards", str); + } + } + + if (const char* str = op.getStringOption("ion-optimize-gcbarriers")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableRedundantGCBarriers = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableRedundantGCBarriers = true; + } else { + return OptionFailure("ion-optimize-gcbarriers", str); + } + } + + if (const char* str = op.getStringOption("ion-instruction-reordering")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableInstructionReordering = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableInstructionReordering = true; + } else { + return OptionFailure("ion-instruction-reordering", str); + } + } + + if (op.getBoolOption("ion-check-range-analysis")) { + jit::JitOptions.checkRangeAnalysis = true; + } + + if (op.getBoolOption("ion-extra-checks")) { + jit::JitOptions.runExtraChecks = true; + } + + if (const char* str = op.getStringOption("ion-inlining")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableInlining = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableInlining = true; + } else { + return OptionFailure("ion-inlining", str); + } + } + + if (const char* str = op.getStringOption("ion-osr")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.osr = true; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.osr = false; + } else { + return OptionFailure("ion-osr", str); + } + } + + if (const char* str = op.getStringOption("ion-limit-script-size")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.limitScriptSize = true; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.limitScriptSize = false; + } else { + return OptionFailure("ion-limit-script-size", str); + } + } + + int32_t warmUpThreshold = op.getIntOption("ion-warmup-threshold"); + if (warmUpThreshold >= 0) { + jit::JitOptions.setNormalIonWarmUpThreshold(warmUpThreshold); + } + + warmUpThreshold = op.getIntOption("baseline-warmup-threshold"); + if (warmUpThreshold >= 0) { + jit::JitOptions.baselineJitWarmUpThreshold = warmUpThreshold; + } + + warmUpThreshold = op.getIntOption("trial-inlining-warmup-threshold"); + if (warmUpThreshold >= 0) { + jit::JitOptions.trialInliningWarmUpThreshold = warmUpThreshold; + } + + warmUpThreshold = op.getIntOption("regexp-warmup-threshold"); + if (warmUpThreshold >= 0) { + jit::JitOptions.regexpWarmUpThreshold = warmUpThreshold; + } + + if (op.getBoolOption("baseline-eager")) { + jit::JitOptions.setEagerBaselineCompilation(); + } + + if (op.getBoolOption("blinterp")) { + jit::JitOptions.baselineInterpreter = true; + } + + if (op.getBoolOption("no-blinterp")) { + jit::JitOptions.baselineInterpreter = false; + } + + if (op.getBoolOption("disable-jithints")) { + jit::JitOptions.disableJitHints = true; + } + + if (op.getBoolOption("emit-interpreter-entry")) { + jit::JitOptions.emitInterpreterEntryTrampoline = true; + } + + if (op.getBoolOption("no-emit-interpreter-entry")) { + jit::JitOptions.emitInterpreterEntryTrampoline = false; + } + + warmUpThreshold = op.getIntOption("blinterp-warmup-threshold"); + if (warmUpThreshold >= 0) { + jit::JitOptions.baselineInterpreterWarmUpThreshold = warmUpThreshold; + } + + if (op.getBoolOption("blinterp-eager")) { + jit::JitOptions.baselineInterpreterWarmUpThreshold = 0; + } + + if (op.getBoolOption("no-baseline")) { + jit::JitOptions.baselineJit = false; + } + + if (op.getBoolOption("no-ion")) { + jit::JitOptions.ion = false; + } + + if (op.getBoolOption("no-native-regexp")) { + jit::JitOptions.nativeRegExp = false; + } + + if (op.getBoolOption("trace-regexp-parser")) { + jit::JitOptions.trace_regexp_parser = true; + } + if (op.getBoolOption("trace-regexp-assembler")) { + jit::JitOptions.trace_regexp_assembler = true; + } + if (op.getBoolOption("trace-regexp-interpreter")) { + jit::JitOptions.trace_regexp_bytecodes = true; + } + if (op.getBoolOption("trace-regexp-peephole")) { + jit::JitOptions.trace_regexp_peephole_optimization = true; + } + + if (op.getBoolOption("less-debug-code")) { + jit::JitOptions.lessDebugCode = true; + } + + int32_t inliningEntryThreshold = op.getIntOption("inlining-entry-threshold"); + if (inliningEntryThreshold > 0) { + jit::JitOptions.inliningEntryThreshold = inliningEntryThreshold; + } + + int32_t smallFunctionLength = op.getIntOption("small-function-length"); + if (smallFunctionLength > 0) { + jit::JitOptions.smallFunctionMaxBytecodeLength = smallFunctionLength; + } + + if (const char* str = op.getStringOption("ion-regalloc")) { + jit::JitOptions.forcedRegisterAllocator = jit::LookupRegisterAllocator(str); + if (!jit::JitOptions.forcedRegisterAllocator.isSome()) { + return OptionFailure("ion-regalloc", str); + } + } + + if (op.getBoolOption("ion-eager")) { + jit::JitOptions.setEagerIonCompilation(); + } + + offthreadCompilation = true; + if (const char* str = op.getStringOption("ion-offthread-compile")) { + if (strcmp(str, "off") == 0) { + offthreadCompilation = false; + } else if (strcmp(str, "on") != 0) { + return OptionFailure("ion-offthread-compile", str); + } + } + cx->runtime()->setOffthreadIonCompilationEnabled(offthreadCompilation); + + if (op.getStringOption("ion-parallel-compile")) { + fprintf(stderr, + "--ion-parallel-compile is deprecated. Please use " + "--ion-offthread-compile instead.\n"); + return false; + } + + if (op.getBoolOption("disable-bailout-loop-check")) { + jit::JitOptions.disableBailoutLoopCheck = true; + } + + if (op.getBoolOption("enable-watchtower")) { + jit::JitOptions.enableWatchtowerMegamorphic = true; + } + if (op.getBoolOption("disable-watchtower")) { + jit::JitOptions.enableWatchtowerMegamorphic = false; + } + if (op.getBoolOption("only-inline-selfhosted")) { + jit::JitOptions.onlyInlineSelfHosted = true; + } + + if (op.getBoolOption("enable-ic-frame-pointers")) { + jit::JitOptions.enableICFramePointers = true; + } + + if (const char* str = op.getStringOption("ion-iterator-indices")) { + if (strcmp(str, "on") == 0) { + jit::JitOptions.disableIteratorIndices = false; + } else if (strcmp(str, "off") == 0) { + jit::JitOptions.disableIteratorIndices = true; + } else { + return OptionFailure("ion-iterator-indices", str); + } + } + +#if defined(JS_SIMULATOR_ARM) + if (op.getBoolOption("arm-sim-icache-checks")) { + jit::SimulatorProcess::ICacheCheckingDisableCount = 0; + } + + int32_t stopAt = op.getIntOption("arm-sim-stop-at"); + if (stopAt >= 0) { + jit::Simulator::StopSimAt = stopAt; + } +#elif defined(JS_SIMULATOR_MIPS32) || defined(JS_SIMULATOR_MIPS64) + if (op.getBoolOption("mips-sim-icache-checks")) { + jit::SimulatorProcess::ICacheCheckingDisableCount = 0; + } + + int32_t stopAt = op.getIntOption("mips-sim-stop-at"); + if (stopAt >= 0) { + jit::Simulator::StopSimAt = stopAt; + } +#elif defined(JS_SIMULATOR_LOONG64) + if (op.getBoolOption("loong64-sim-icache-checks")) { + jit::SimulatorProcess::ICacheCheckingDisableCount = 0; + } + + int32_t stopAt = op.getIntOption("loong64-sim-stop-at"); + if (stopAt >= 0) { + jit::Simulator::StopSimAt = stopAt; + } +#endif + +#ifdef DEBUG +# ifdef JS_CODEGEN_RISCV64 + if (op.getBoolOption("riscv-debug")) { + jit::Assembler::FLAG_riscv_debug = true; + } +# endif +# ifdef JS_SIMULATOR_RISCV64 + if (op.getBoolOption("trace-sim")) { + jit::Simulator::FLAG_trace_sim = true; + } + if (op.getBoolOption("debug-sim")) { + jit::Simulator::FLAG_debug_sim = true; + } + if (op.getBoolOption("riscv-trap-to-simulator-debugger")) { + jit::Simulator::FLAG_riscv_trap_to_simulator_debugger = true; + } + int32_t stopAt = op.getIntOption("riscv-sim-stop-at"); + if (stopAt >= 0) { + jit::Simulator::StopSimAt = stopAt; + } +# endif +#endif + + return true; +} + +bool SetContextGCOptions(JSContext* cx, const OptionParser& op) { + JS_SetGCParameter(cx, JSGC_MAX_BYTES, 0xffffffff); + + size_t nurseryBytes = op.getIntOption("nursery-size") * 1024L * 1024L; + if (nurseryBytes == 0) { + fprintf(stderr, "Error: --nursery-size parameter must be non-zero.\n"); + fprintf(stderr, + "The nursery can be disabled by passing the --no-ggc option.\n"); + return false; + } + JS_SetGCParameter(cx, JSGC_MAX_NURSERY_BYTES, nurseryBytes); + + size_t availMemMB = op.getIntOption("available-memory"); + if (availMemMB > 0) { + JS_SetGCParametersBasedOnAvailableMemory(cx, availMemMB); + } + + if (const char* opt = op.getStringOption("nursery-strings")) { + if (strcmp(opt, "on") == 0) { + cx->runtime()->gc.nursery().enableStrings(); + } else if (strcmp(opt, "off") == 0) { + cx->runtime()->gc.nursery().disableStrings(); + } else { + MOZ_CRASH("invalid option value for --nursery-strings, must be on/off"); + } + } + + if (const char* opt = op.getStringOption("nursery-bigints")) { + if (strcmp(opt, "on") == 0) { + cx->runtime()->gc.nursery().enableBigInts(); + } else if (strcmp(opt, "off") == 0) { + cx->runtime()->gc.nursery().disableBigInts(); + } else { + MOZ_CRASH("invalid option value for --nursery-bigints, must be on/off"); + } + } + + bool incrementalGC = !op.getBoolOption("no-incremental-gc"); + JS_SetGCParameter(cx, JSGC_INCREMENTAL_GC_ENABLED, incrementalGC); + + if (op.getBoolOption("enable-parallel-marking")) { + JS_SetGCParameter(cx, JSGC_PARALLEL_MARKING_ENABLED, true); + } + int32_t markingThreads = op.getIntOption("marking-threads"); + if (markingThreads > 0) { + JS_SetGCParameter(cx, JSGC_MARKING_THREAD_COUNT, markingThreads); + } + + JS_SetGCParameter(cx, JSGC_SLICE_TIME_BUDGET_MS, 5); + + JS_SetGCParameter(cx, JSGC_PER_ZONE_GC_ENABLED, true); + + for (MultiStringRange args = op.getMultiStringOption("gc-param"); + !args.empty(); args.popFront()) { + if (!SetGCParameterFromArg(cx, args.front())) { + return false; + } + } + +#ifdef DEBUG + dumpEntrainedVariables = op.getBoolOption("dump-entrained-variables"); +#endif + +#ifdef JS_GC_ZEAL + const char* zealStr = op.getStringOption("gc-zeal"); + if (zealStr) { + if (!cx->runtime()->gc.parseAndSetZeal(zealStr)) { + return false; + } + uint32_t nextScheduled; + cx->runtime()->gc.getZealBits(&gZealBits, &gZealFrequency, &nextScheduled); + } +#endif + + return true; +} + +bool InitModuleLoader(JSContext* cx, const OptionParser& op) { + RootedString moduleLoadPath(cx); + if (const char* option = op.getStringOption("module-load-path")) { + UniqueChars pathUtf8 = JS::EncodeNarrowToUtf8(cx, option); + if (!pathUtf8) { + return false; + } + + Rooted<JSString*> jspath(cx, NewStringCopyUTF8(cx, pathUtf8.get())); + if (!jspath) { + return false; + } + + moduleLoadPath = js::shell::ResolvePath(cx, jspath, RootRelative); + + processWideModuleLoadPath = JS_EncodeStringToUTF8(cx, moduleLoadPath); + if (!processWideModuleLoadPath) { + return false; + } + } else { + processWideModuleLoadPath = js::shell::GetCWD(cx); + if (!processWideModuleLoadPath) { + return false; + } + + moduleLoadPath = NewStringCopyUTF8(cx, processWideModuleLoadPath.get()); + if (!moduleLoadPath) { + return false; + } + } + + ShellContext* sc = GetShellContext(cx); + sc->moduleLoader = js::MakeUnique<ModuleLoader>(); + if (!sc->moduleLoader || !sc->moduleLoader->init(cx, moduleLoadPath)) { + return false; + } + + return true; +} diff --git a/js/src/shell/jsoptparse.cpp b/js/src/shell/jsoptparse.cpp new file mode 100644 index 0000000000..5632598d7d --- /dev/null +++ b/js/src/shell/jsoptparse.cpp @@ -0,0 +1,639 @@ +/* -*- 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/. */ + +#include "shell/jsoptparse.h" + +#include <algorithm> +#include <stdarg.h> +#include <string_view> + +#include "util/Unicode.h" + +using namespace js; +using namespace js::cli; +using namespace js::cli::detail; + +#define OPTION_CONVERT_IMPL(__cls) \ + bool Option::is##__cls##Option() const { return kind == OptionKind##__cls; } \ + __cls##Option* Option::as##__cls##Option() { \ + MOZ_ASSERT(is##__cls##Option()); \ + return static_cast<__cls##Option*>(this); \ + } \ + const __cls##Option* Option::as##__cls##Option() const { \ + return const_cast<Option*>(this)->as##__cls##Option(); \ + } + +ValuedOption* Option::asValued() { + MOZ_ASSERT(isValued()); + return static_cast<ValuedOption*>(this); +} + +const ValuedOption* Option::asValued() const { + return const_cast<Option*>(this)->asValued(); +} + +OPTION_CONVERT_IMPL(Bool) +OPTION_CONVERT_IMPL(String) +OPTION_CONVERT_IMPL(Int) +OPTION_CONVERT_IMPL(MultiString) + +void OptionParser::setArgTerminatesOptions(const char* name, bool enabled) { + findArgument(name)->setTerminatesOptions(enabled); +} + +void OptionParser::setArgCapturesRest(const char* name) { + MOZ_ASSERT(restArgument == -1, + "only one argument may be set to capture the rest"); + restArgument = findArgumentIndex(name); + MOZ_ASSERT(restArgument != -1, + "unknown argument name passed to setArgCapturesRest"); +} + +OptionParser::Result OptionParser::error(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + fprintf(stderr, "Error: "); + vfprintf(stderr, fmt, args); + va_end(args); + fputs("\n\n", stderr); + return ParseError; +} + +/* Quick and dirty paragraph printer. */ +static void PrintParagraph(const char* text, unsigned startColno, + const unsigned limitColno, bool padFirstLine) { + unsigned colno = startColno; + unsigned indent = 0; + const char* it = text; + + if (padFirstLine) { + printf("%*s", int(startColno), ""); + } + + /* Skip any leading spaces. */ + while (*it != '\0' && unicode::IsSpace(*it)) { + ++it; + } + + while (*it != '\0') { + MOZ_ASSERT(!unicode::IsSpace(*it) || *it == '\n'); + + /* Delimit the current token. */ + const char* limit = it; + while (!unicode::IsSpace(*limit) && *limit != '\0') { + ++limit; + } + + /* + * If the current token is longer than the available number of columns, + * then make a line break before printing the token. + */ + size_t tokLen = limit - it; + if (tokLen + colno >= limitColno) { + printf("\n%*s%.*s", int(startColno + indent), "", int(tokLen), it); + colno = startColno + tokLen; + } else { + printf("%.*s", int(tokLen), it); + colno += tokLen; + } + + switch (*limit) { + case '\0': + return; + case ' ': + putchar(' '); + colno += 1; + it = limit; + while (*it == ' ') { + ++it; + } + break; + case '\n': + /* |text| wants to force a newline here. */ + printf("\n%*s", int(startColno), ""); + colno = startColno; + it = limit + 1; + /* Could also have line-leading spaces. */ + indent = 0; + while (*it == ' ') { + putchar(' '); + ++colno; + ++indent; + ++it; + } + break; + default: + MOZ_CRASH("unhandled token splitting character in text"); + } + } +} + +static const char* OptionFlagsToFormatInfo(char shortflag, bool isValued, + size_t* length) { + static const char* const fmt[4] = {" -%c --%s ", " --%s ", " -%c --%s=%s ", + " --%s=%s "}; + + /* How mny chars w/o longflag? */ + size_t lengths[4] = {strlen(fmt[0]) - 3, strlen(fmt[1]) - 3, + strlen(fmt[2]) - 5, strlen(fmt[3]) - 5}; + int index = isValued ? 2 : 0; + if (!shortflag) { + index++; + } + + *length = lengths[index]; + return fmt[index]; +} + +OptionParser::Result OptionParser::printHelp(const char* progname) { + constexpr std::string_view prognameMeta = "{progname}"; + + const char* prefixEnd = strstr(usage, prognameMeta.data()); + if (prefixEnd) { + printf("%.*s%s%s\n", int(prefixEnd - usage), usage, progname, + prefixEnd + prognameMeta.length()); + } else { + puts(usage); + } + + if (descr) { + putchar('\n'); + PrintParagraph(descr, 2, descrWidth, true); + putchar('\n'); + } + + if (version) { + printf("\nVersion: %s\n\n", version); + } + + if (!arguments.empty()) { + printf("Arguments:\n"); + + static const char fmt[] = " %s "; + size_t fmtChars = sizeof(fmt) - 2; + size_t lhsLen = 0; + for (Option* arg : arguments) { + lhsLen = std::max(lhsLen, strlen(arg->longflag) + fmtChars); + } + + for (Option* arg : arguments) { + size_t chars = printf(fmt, arg->longflag); + for (; chars < lhsLen; ++chars) { + putchar(' '); + } + PrintParagraph(arg->help, lhsLen, helpWidth, false); + putchar('\n'); + } + putchar('\n'); + } + + if (!options.empty()) { + printf("Options:\n"); + + /* Calculate sizes for column alignment. */ + size_t lhsLen = 0; + for (Option* opt : options) { + size_t longflagLen = strlen(opt->longflag); + + size_t fmtLen; + OptionFlagsToFormatInfo(opt->shortflag, opt->isValued(), &fmtLen); + + size_t len = fmtLen + longflagLen; + if (opt->isValued()) { + len += strlen(opt->asValued()->metavar); + } + lhsLen = std::max(lhsLen, len); + } + + /* Print option help text. */ + for (Option* opt : options) { + size_t fmtLen; + const char* fmt = + OptionFlagsToFormatInfo(opt->shortflag, opt->isValued(), &fmtLen); + size_t chars; + if (opt->isValued()) { + if (opt->shortflag) { + chars = printf(fmt, opt->shortflag, opt->longflag, + opt->asValued()->metavar); + } else { + chars = printf(fmt, opt->longflag, opt->asValued()->metavar); + } + } else { + if (opt->shortflag) { + chars = printf(fmt, opt->shortflag, opt->longflag); + } else { + chars = printf(fmt, opt->longflag); + } + } + for (; chars < lhsLen; ++chars) { + putchar(' '); + } + PrintParagraph(opt->help, lhsLen, helpWidth, false); + putchar('\n'); + } + } + + return EarlyExit; +} + +OptionParser::Result OptionParser::printVersion() { + MOZ_ASSERT(version); + printf("%s\n", version); + return EarlyExit; +} + +OptionParser::Result OptionParser::extractValue(size_t argc, char** argv, + size_t* i, char** value) { + MOZ_ASSERT(*i < argc); + char* eq = strchr(argv[*i], '='); + if (eq) { + *value = eq + 1; + if (*value[0] == '\0') { + return error("A value is required for option %.*s", (int)(eq - argv[*i]), + argv[*i]); + } + return Okay; + } + + if (argc == *i + 1) { + return error("Expected a value for option %s", argv[*i]); + } + + *i += 1; + *value = argv[*i]; + return Okay; +} + +OptionParser::Result OptionParser::handleOption(Option* opt, size_t argc, + char** argv, size_t* i, + bool* optionsAllowed) { + if (opt->getTerminatesOptions()) { + *optionsAllowed = false; + } + + switch (opt->kind) { + case OptionKindBool: { + if (opt == &helpOption) { + return printHelp(argv[0]); + } + if (opt == &versionOption) { + return printVersion(); + } + opt->asBoolOption()->value = true; + return Okay; + } + /* + * Valued options are allowed to specify their values either via + * successive arguments or a single --longflag=value argument. + */ + case OptionKindString: { + char* value = nullptr; + if (Result r = extractValue(argc, argv, i, &value)) { + return r; + } + opt->asStringOption()->value = value; + return Okay; + } + case OptionKindInt: { + char* value = nullptr; + if (Result r = extractValue(argc, argv, i, &value)) { + return r; + } + opt->asIntOption()->value = atoi(value); + return Okay; + } + case OptionKindMultiString: { + char* value = nullptr; + if (Result r = extractValue(argc, argv, i, &value)) { + return r; + } + StringArg arg(value, *i); + return opt->asMultiStringOption()->strings.append(arg) ? Okay : Fail; + } + default: + MOZ_CRASH("unhandled option kind"); + } +} + +OptionParser::Result OptionParser::handleArg(size_t argc, char** argv, + size_t* i, bool* optionsAllowed) { + if (nextArgument >= arguments.length()) { + return error("Too many arguments provided"); + } + + Option* arg = arguments[nextArgument]; + + if (arg->getTerminatesOptions()) { + *optionsAllowed = false; + } + + switch (arg->kind) { + case OptionKindString: + arg->asStringOption()->value = argv[*i]; + nextArgument += 1; + return Okay; + case OptionKindMultiString: { + // Don't advance the next argument -- there can only be one (final) + // variadic argument. + StringArg value(argv[*i], *i); + return arg->asMultiStringOption()->strings.append(value) ? Okay : Fail; + } + default: + MOZ_CRASH("unhandled argument kind"); + } +} + +OptionParser::Result OptionParser::parseArgs(int inputArgc, char** argv) { + MOZ_ASSERT(inputArgc >= 0); + size_t argc = inputArgc; + // Permit a "no more options" capability, like |--| offers in many shell + // interfaces. + bool optionsAllowed = true; + + for (size_t i = 1; i < argc; ++i) { + char* arg = argv[i]; + Result r; + /* Note: solo dash option is actually a 'stdin' argument. */ + if (arg[0] == '-' && arg[1] != '\0' && optionsAllowed) { + /* Option. */ + Option* opt; + if (arg[1] == '-') { + if (arg[2] == '\0') { + /* End of options */ + optionsAllowed = false; + nextArgument = restArgument; + continue; + } else { + /* Long option. */ + opt = findOption(arg + 2); + if (!opt) { + return error("Invalid long option: %s", arg); + } + } + } else { + /* Short option */ + if (arg[2] != '\0') { + return error("Short option followed by junk: %s", arg); + } + opt = findOption(arg[1]); + if (!opt) { + return error("Invalid short option: %s", arg); + } + } + + r = handleOption(opt, argc, argv, &i, &optionsAllowed); + } else { + /* Argument. */ + r = handleArg(argc, argv, &i, &optionsAllowed); + } + + if (r != Okay) { + return r; + } + } + return Okay; +} + +void OptionParser::setHelpOption(char shortflag, const char* longflag, + const char* help) { + helpOption.setFlagInfo(shortflag, longflag, help); +} + +bool OptionParser::getHelpOption() const { return helpOption.value; } + +bool OptionParser::getBoolOption(char shortflag) const { + return tryFindOption(shortflag)->asBoolOption()->value; +} + +int OptionParser::getIntOption(char shortflag) const { + return tryFindOption(shortflag)->asIntOption()->value; +} + +const char* OptionParser::getStringOption(char shortflag) const { + return tryFindOption(shortflag)->asStringOption()->value; +} + +MultiStringRange OptionParser::getMultiStringOption(char shortflag) const { + const MultiStringOption* mso = + tryFindOption(shortflag)->asMultiStringOption(); + return MultiStringRange(mso->strings.begin(), mso->strings.end()); +} + +bool OptionParser::getBoolOption(const char* longflag) const { + return tryFindOption(longflag)->asBoolOption()->value; +} + +int OptionParser::getIntOption(const char* longflag) const { + return tryFindOption(longflag)->asIntOption()->value; +} + +const char* OptionParser::getStringOption(const char* longflag) const { + return tryFindOption(longflag)->asStringOption()->value; +} + +MultiStringRange OptionParser::getMultiStringOption( + const char* longflag) const { + const MultiStringOption* mso = tryFindOption(longflag)->asMultiStringOption(); + return MultiStringRange(mso->strings.begin(), mso->strings.end()); +} + +OptionParser::~OptionParser() { + for (Option* opt : options) { + js_delete<Option>(opt); + } + for (Option* arg : arguments) { + js_delete<Option>(arg); + } +} + +Option* OptionParser::findOption(char shortflag) { + for (Option* opt : options) { + if (opt->shortflag == shortflag) { + return opt; + } + } + + if (versionOption.shortflag == shortflag) { + return &versionOption; + } + + return helpOption.shortflag == shortflag ? &helpOption : nullptr; +} + +const Option* OptionParser::findOption(char shortflag) const { + return const_cast<OptionParser*>(this)->findOption(shortflag); +} + +const Option* OptionParser::tryFindOption(char shortflag) const { + const Option* maybeOption = findOption(shortflag); + if (!maybeOption) { + fprintf(stderr, "Failed to find short option %c\n", shortflag); + MOZ_CRASH(); + } + return maybeOption; +} + +Option* OptionParser::findOption(const char* longflag) { + for (Option* opt : options) { + const char* target = opt->longflag; + if (opt->isValued()) { + size_t targetLen = strlen(target); + /* Permit a trailing equals sign on the longflag argument. */ + for (size_t i = 0; i < targetLen; ++i) { + if (longflag[i] == '\0' || longflag[i] != target[i]) { + goto no_match; + } + } + if (longflag[targetLen] == '\0' || longflag[targetLen] == '=') { + return opt; + } + } else { + if (strcmp(target, longflag) == 0) { + return opt; + } + } + no_match:; + } + + if (strcmp(versionOption.longflag, longflag) == 0) { + return &versionOption; + } + + return strcmp(helpOption.longflag, longflag) ? nullptr : &helpOption; +} + +const Option* OptionParser::findOption(const char* longflag) const { + return const_cast<OptionParser*>(this)->findOption(longflag); +} + +const Option* OptionParser::tryFindOption(const char* longflag) const { + const Option* maybeOption = findOption(longflag); + if (!maybeOption) { + fprintf(stderr, "Failed to find long option %s\n", longflag); + MOZ_CRASH(); + } + return maybeOption; +} + +/* Argument accessors */ + +int OptionParser::findArgumentIndex(const char* name) const { + for (Option* const* it = arguments.begin(); it != arguments.end(); ++it) { + const char* target = (*it)->longflag; + if (strcmp(target, name) == 0) { + return it - arguments.begin(); + } + } + return -1; +} + +Option* OptionParser::findArgument(const char* name) { + int index = findArgumentIndex(name); + return (index == -1) ? nullptr : arguments[index]; +} + +const Option* OptionParser::findArgument(const char* name) const { + int index = findArgumentIndex(name); + return (index == -1) ? nullptr : arguments[index]; +} + +const char* OptionParser::getStringArg(const char* name) const { + return findArgument(name)->asStringOption()->value; +} + +MultiStringRange OptionParser::getMultiStringArg(const char* name) const { + const MultiStringOption* mso = findArgument(name)->asMultiStringOption(); + return MultiStringRange(mso->strings.begin(), mso->strings.end()); +} + +/* Option builders */ + +// Use vanilla malloc for allocations. See OptionAllocPolicy. +JS_DECLARE_NEW_METHODS(opt_new, malloc, static MOZ_ALWAYS_INLINE) + +bool OptionParser::addIntOption(char shortflag, const char* longflag, + const char* metavar, const char* help, + int defaultValue) { + if (!options.reserve(options.length() + 1)) { + return false; + } + IntOption* io = + opt_new<IntOption>(shortflag, longflag, help, metavar, defaultValue); + if (!io) { + return false; + } + options.infallibleAppend(io); + return true; +} + +bool OptionParser::addBoolOption(char shortflag, const char* longflag, + const char* help) { + if (!options.reserve(options.length() + 1)) { + return false; + } + BoolOption* bo = opt_new<BoolOption>(shortflag, longflag, help); + if (!bo) { + return false; + } + options.infallibleAppend(bo); + return true; +} + +bool OptionParser::addStringOption(char shortflag, const char* longflag, + const char* metavar, const char* help) { + if (!options.reserve(options.length() + 1)) { + return false; + } + StringOption* so = opt_new<StringOption>(shortflag, longflag, help, metavar); + if (!so) { + return false; + } + options.infallibleAppend(so); + return true; +} + +bool OptionParser::addMultiStringOption(char shortflag, const char* longflag, + const char* metavar, const char* help) { + if (!options.reserve(options.length() + 1)) { + return false; + } + MultiStringOption* mso = + opt_new<MultiStringOption>(shortflag, longflag, help, metavar); + if (!mso) { + return false; + } + options.infallibleAppend(mso); + return true; +} + +/* Argument builders */ + +bool OptionParser::addOptionalStringArg(const char* name, const char* help) { + if (!arguments.reserve(arguments.length() + 1)) { + return false; + } + StringOption* so = opt_new<StringOption>(1, name, help, (const char*)nullptr); + if (!so) { + return false; + } + arguments.infallibleAppend(so); + return true; +} + +bool OptionParser::addOptionalMultiStringArg(const char* name, + const char* help) { + MOZ_ASSERT_IF(!arguments.empty(), !arguments.back()->isVariadic()); + if (!arguments.reserve(arguments.length() + 1)) { + return false; + } + MultiStringOption* mso = + opt_new<MultiStringOption>(1, name, help, (const char*)nullptr); + if (!mso) { + return false; + } + arguments.infallibleAppend(mso); + return true; +} diff --git a/js/src/shell/jsoptparse.h b/js/src/shell/jsoptparse.h new file mode 100644 index 0000000000..17e4878969 --- /dev/null +++ b/js/src/shell/jsoptparse.h @@ -0,0 +1,336 @@ +/* -*- 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_jsoptparse_h +#define shell_jsoptparse_h + +#include <stdio.h> + +#include "js/AllocPolicy.h" +#include "js/Utility.h" +#include "js/Vector.h" + +namespace js { +namespace cli { + +namespace detail { + +// We want to use the shell's option parser before initializing the JS engine. +// The JS malloc arena isn't available yet at this point, so we use a custom +// allocation policy that uses the system malloc instead. +class OptionAllocPolicy { + public: + template <typename T> + T* pod_malloc(size_t numElems) { + size_t bytes; + if (MOZ_UNLIKELY(!js::CalculateAllocSize<T>(numElems, &bytes))) { + return nullptr; + } + return static_cast<T*>(malloc(bytes)); + } + + template <typename T> + T* pod_realloc(T* p, size_t oldSize, size_t newSize) { + size_t bytes; + if (MOZ_UNLIKELY(!js::CalculateAllocSize<T>(newSize, &bytes))) { + return nullptr; + } + return static_cast<T*>(realloc(p, bytes)); + } + + void reportAllocOverflow() const {} + bool checkSimulatedOOM() const { return !js::oom::ShouldFailWithOOM(); } + + template <typename T> + void free_(T* p, size_t numElems = 0) { + free(p); + } +}; + +struct BoolOption; +struct MultiStringOption; +struct ValuedOption; +struct StringOption; +struct IntOption; + +enum OptionKind { + OptionKindBool, + OptionKindString, + OptionKindInt, + OptionKindMultiString, + OptionKindInvalid +}; + +struct Option { + const char* longflag; + const char* help; + OptionKind kind; + char shortflag; + bool terminatesOptions; + + Option(OptionKind kind, char shortflag, const char* longflag, + const char* help) + : longflag(longflag), + help(help), + kind(kind), + shortflag(shortflag), + terminatesOptions(false) {} + + virtual ~Option() = 0; + + void setTerminatesOptions(bool enabled) { terminatesOptions = enabled; } + bool getTerminatesOptions() const { return terminatesOptions; } + + virtual bool isValued() const { return false; } + + /* Only some valued options are variadic (like MultiStringOptions). */ + virtual bool isVariadic() const { return false; } + + /* + * For arguments, the shortflag field is used to indicate whether the + * argument is optional. + */ + bool isOptional() { return shortflag; } + + void setFlagInfo(char shortflag, const char* longflag, const char* help) { + this->shortflag = shortflag; + this->longflag = longflag; + this->help = help; + } + + ValuedOption* asValued(); + const ValuedOption* asValued() const; + +#define OPTION_CONVERT_DECL(__cls) \ + bool is##__cls##Option() const; \ + __cls##Option* as##__cls##Option(); \ + const __cls##Option* as##__cls##Option() const; + + OPTION_CONVERT_DECL(Bool) + OPTION_CONVERT_DECL(String) + OPTION_CONVERT_DECL(Int) + OPTION_CONVERT_DECL(MultiString) +}; + +inline Option::~Option() {} + +struct BoolOption : public Option { + size_t argno; + bool value; + + BoolOption(char shortflag, const char* longflag, const char* help) + : Option(OptionKindBool, shortflag, longflag, help), value(false) {} + + virtual ~BoolOption() {} +}; + +struct ValuedOption : public Option { + const char* metavar; + + ValuedOption(OptionKind kind, char shortflag, const char* longflag, + const char* help, const char* metavar) + : Option(kind, shortflag, longflag, help), metavar(metavar) {} + + virtual ~ValuedOption() = 0; + virtual bool isValued() const override { return true; } +}; + +inline ValuedOption::~ValuedOption() {} + +struct StringOption : public ValuedOption { + const char* value; + + StringOption(char shortflag, const char* longflag, const char* help, + const char* metavar) + : ValuedOption(OptionKindString, shortflag, longflag, help, metavar), + value(nullptr) {} + + virtual ~StringOption() {} +}; + +struct IntOption : public ValuedOption { + int value; + + IntOption(char shortflag, const char* longflag, const char* help, + const char* metavar, int defaultValue) + : ValuedOption(OptionKindInt, shortflag, longflag, help, metavar), + value(defaultValue) {} + + virtual ~IntOption() {} +}; + +struct StringArg { + char* value; + size_t argno; + + StringArg(char* value, size_t argno) : value(value), argno(argno) {} +}; + +struct MultiStringOption : public ValuedOption { + Vector<StringArg, 0, detail::OptionAllocPolicy> strings; + + MultiStringOption(char shortflag, const char* longflag, const char* help, + const char* metavar) + : ValuedOption(OptionKindMultiString, shortflag, longflag, help, + metavar) {} + + virtual ~MultiStringOption() {} + + virtual bool isVariadic() const override { return true; } +}; + +} /* namespace detail */ + +class MultiStringRange { + typedef detail::StringArg StringArg; + const StringArg* cur; + const StringArg* end; + + public: + explicit MultiStringRange(const StringArg* cur, const StringArg* end) + : cur(cur), end(end) { + MOZ_ASSERT(end - cur >= 0); + } + + bool empty() const { return cur == end; } + void popFront() { + MOZ_ASSERT(!empty()); + ++cur; + } + char* front() const { + MOZ_ASSERT(!empty()); + return cur->value; + } + size_t argno() const { + MOZ_ASSERT(!empty()); + return cur->argno; + } +}; + +/* + * Builder for describing a command line interface and parsing the resulting + * specification. + * + * - A multi-option is an option that can appear multiple times and still + * parse as valid command line arguments. + * - An "optional argument" is supported for backwards compatibility with prior + * command line interface usage. Once one optional argument has been added, + * *only* optional arguments may be added. + */ +class OptionParser { + public: + enum Result { + Okay = 0, + Fail, /* As in, allocation fail. */ + ParseError, /* Successfully parsed but with an error. */ + EarlyExit /* Successfully parsed but exits the program, + * for example with --help and --version. */ + }; + + private: + typedef Vector<detail::Option*, 0, detail::OptionAllocPolicy> Options; + typedef detail::Option Option; + typedef detail::BoolOption BoolOption; + + Options options; + Options arguments; + BoolOption helpOption; + BoolOption versionOption; + const char* usage; + const char* version; + const char* descr; + size_t descrWidth; + size_t helpWidth; + size_t nextArgument; + + // If '--' is passed, all remaining arguments should be interpreted as the + // argument at index 'restArgument'. Defaults to the next unassigned + // argument. + int restArgument; + + Option* findOption(char shortflag); + const Option* findOption(char shortflag) const; + const Option* tryFindOption(char shortflag) const; + Option* findOption(const char* longflag); + const Option* findOption(const char* longflag) const; + const Option* tryFindOption(const char* longflag) const; + int findArgumentIndex(const char* name) const; + Option* findArgument(const char* name); + const Option* findArgument(const char* name) const; + + Result error(const char* fmt, ...) MOZ_FORMAT_PRINTF(2, 3); + Result extractValue(size_t argc, char** argv, size_t* i, char** value); + Result handleArg(size_t argc, char** argv, size_t* i, bool* optsAllowed); + Result handleOption(Option* opt, size_t argc, char** argv, size_t* i, + bool* optsAllowed); + + public: + explicit OptionParser(const char* usage) + : helpOption('h', "help", "Display help information"), + versionOption('v', "version", "Display version information and exit"), + usage(usage), + version(nullptr), + descr(nullptr), + descrWidth(80), + helpWidth(80), + nextArgument(0), + restArgument(-1) {} + + ~OptionParser(); + + Result parseArgs(int argc, char** argv); + Result printHelp(const char* progname); + Result printVersion(); + + /* Metadata */ + + void setVersion(const char* v) { version = v; } + void setHelpWidth(size_t width) { helpWidth = width; } + void setDescriptionWidth(size_t width) { descrWidth = width; } + void setDescription(const char* description) { descr = description; } + void setHelpOption(char shortflag, const char* longflag, const char* help); + void setArgTerminatesOptions(const char* name, bool enabled); + void setArgCapturesRest(const char* name); + + /* Arguments: no further arguments may be added after a variadic argument. */ + + bool addOptionalStringArg(const char* name, const char* help); + bool addOptionalMultiStringArg(const char* name, const char* help); + + const char* getStringArg(const char* name) const; + MultiStringRange getMultiStringArg(const char* name) const; + + /* Options */ + + bool addBoolOption(char shortflag, const char* longflag, const char* help); + bool addStringOption(char shortflag, const char* longflag, const char* help, + const char* metavar); + bool addIntOption(char shortflag, const char* longflag, const char* help, + const char* metavar, int defaultValue); + bool addMultiStringOption(char shortflag, const char* longflag, + const char* help, const char* metavar); + bool addOptionalVariadicArg(const char* name); + + int getIntOption(char shortflag) const; + int getIntOption(const char* longflag) const; + const char* getStringOption(char shortflag) const; + const char* getStringOption(const char* longflag) const; + bool getBoolOption(char shortflag) const; + bool getBoolOption(const char* longflag) const; + MultiStringRange getMultiStringOption(char shortflag) const; + MultiStringRange getMultiStringOption(const char* longflag) const; + + /* + * Return whether the help option was present (and thus help was already + * displayed during parse_args). + */ + bool getHelpOption() const; +}; + +} /* namespace cli */ +} /* namespace js */ + +#endif /* shell_jsoptparse_h */ diff --git a/js/src/shell/jsrtfuzzing/jsrtfuzzing-example.js b/js/src/shell/jsrtfuzzing/jsrtfuzzing-example.js new file mode 100644 index 0000000000..2ea9dd95ae --- /dev/null +++ b/js/src/shell/jsrtfuzzing/jsrtfuzzing-example.js @@ -0,0 +1,42 @@ +/* -*- 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/. */ + +// This global will hold the current fuzzing buffer for each iteration. +var fuzzBuf; + +function JSFuzzIterate() { + // This function is called per iteration. You must ensure that: + // + // 1) Each of your actions/decisions is only based on fuzzBuf, + // in particular not on Math.random(), Date/Time or other + // external inputs. + // + // 2) Your actions should be deterministic. The same fuzzBuf + // should always lead to the same set of actions/decisions. + // + // 3) You can modify the global where needed, but ensure that + // each iteration is isolated from one another by cleaning + // any modifications to the global after each iteration. + // In particular, iterations must not depend on or influence + // each other in any way (see also 1)). + // + // 4) You must catch all exceptions. + + try { + // This is a very simple UTF-16 string conversion for example purposes only. + let input = String.fromCharCode.apply( + null, + new Uint16Array(fuzzBuf.buffer) + ); + + // Pass the input through the JSON code as an example. Note that this + // particular example could probably be implemented more efficiently + // directly in fuzz-tests on a C++ level. This is purely for demonstration + // purposes. + print(JSON.stringify(JSON.parse(input))); + } catch (exc) { + print(exc); + } +} diff --git a/js/src/shell/jsrtfuzzing/jsrtfuzzing.cpp b/js/src/shell/jsrtfuzzing/jsrtfuzzing.cpp new file mode 100644 index 0000000000..0b6f505fb6 --- /dev/null +++ b/js/src/shell/jsrtfuzzing/jsrtfuzzing.cpp @@ -0,0 +1,139 @@ +/* -*- 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/. */ + +#include "shell/jsrtfuzzing/jsrtfuzzing.h" + +#include "mozilla/Assertions.h" // MOZ_CRASH +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include <stdio.h> // fflush, fprintf, fputs + +#include "FuzzerDefs.h" +#include "FuzzingInterface.h" +#include "jsapi.h" // JS_ClearPendingException, JS_IsExceptionPending + +#include "js/CompilationAndEvaluation.h" // JS::Evaluate +#include "js/CompileOptions.h" // JS::CompileOptions +#include "js/Conversions.h" // JS::ToInt32 +#include "js/ErrorReport.h" // JS::PrintError +#include "js/Exception.h" // JS::StealPendingExceptionStack +#include "js/experimental/TypedData.h" // JS_GetUint8ClampedArrayData, JS_NewUint8ClampedArray +#include "js/PropertyAndElement.h" // JS_SetProperty +#include "js/RootingAPI.h" // JS::Rooted +#include "js/SourceText.h" // JS::Source{Ownership,Text} +#include "js/Value.h" // JS::Value +#include "shell/jsshell.h" // js::shell::{reportWarnings,PrintStackTrace,sArg{c,v}} +#include "util/Text.h" +#include "vm/Interpreter.h" +#include "vm/TypedArrayObject.h" + +#include "vm/ArrayBufferObject-inl.h" +#include "vm/JSContext-inl.h" + +static JSContext* gCx = nullptr; +static std::string gFuzzModuleName; + +static void CrashOnPendingException() { + if (JS_IsExceptionPending(gCx)) { + JS::ExceptionStack exnStack(gCx); + (void)JS::StealPendingExceptionStack(gCx, &exnStack); + + JS::ErrorReportBuilder report(gCx); + if (!report.init(gCx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) { + fprintf(stderr, "out of memory initializing JS::ErrorReportBuilder\n"); + fflush(stderr); + } else { + JS::PrintError(stderr, report, js::shell::reportWarnings); + if (!js::shell::PrintStackTrace(gCx, exnStack.stack())) { + fputs("(Unable to print stack trace)\n", stderr); + } + } + + MOZ_CRASH("Unhandled exception from JS runtime!"); + } +} + +int js::shell::FuzzJSRuntimeStart(JSContext* cx, int* argc, char*** argv) { + gCx = cx; + gFuzzModuleName = getenv("FUZZER"); + + int ret = FuzzJSRuntimeInit(argc, argv); + if (ret) { + fprintf(stderr, "Fuzzing Interface: Error: Initialize callback failed\n"); + return ret; + } + +#ifdef LIBFUZZER + fuzzer::FuzzerDriver(&shell::sArgc, &shell::sArgv, FuzzJSRuntimeFuzz); +#elif AFLFUZZ + MOZ_CRASH("AFL is unsupported for JS runtime fuzzing integration"); +#endif + return 0; +} + +int js::shell::FuzzJSRuntimeInit(int* argc, char*** argv) { + JS::Rooted<JS::Value> v(gCx); + JS::CompileOptions opts(gCx); + + // Load the fuzzing module specified in the FUZZER environment variable + JS::EvaluateUtf8Path(gCx, opts, gFuzzModuleName.c_str(), &v); + + // Any errors while loading the fuzzing module should be fatal + CrashOnPendingException(); + + return 0; +} + +int js::shell::FuzzJSRuntimeFuzz(const uint8_t* buf, size_t size) { + if (!size) { + return 0; + } + + JS::Rooted<JSObject*> arr(gCx, JS_NewUint8ClampedArray(gCx, size)); + if (!arr) { + MOZ_CRASH("OOM"); + } + + do { + JS::AutoCheckCannotGC nogc; + bool isShared; + uint8_t* data = JS_GetUint8ClampedArrayData(arr, &isShared, nogc); + MOZ_RELEASE_ASSERT(!isShared); + memcpy(data, buf, size); + } while (false); + + JS::RootedValue arrVal(gCx, JS::ObjectValue(*arr)); + if (!JS_SetProperty(gCx, gCx->global(), "fuzzBuf", arrVal)) { + MOZ_CRASH("JS_SetProperty failed"); + } + + JS::Rooted<JS::Value> v(gCx); + JS::CompileOptions opts(gCx); + + static const char data[] = "JSFuzzIterate();"; + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + if (!srcBuf.init(gCx, data, js_strlen(data), JS::SourceOwnership::Borrowed)) { + return 1; + } + + if (!JS::Evaluate(gCx, opts.setFileAndLine(__FILE__, __LINE__), srcBuf, &v) && + !JS_IsExceptionPending(gCx)) { + // A return value of `false` without a pending exception indicates + // a timeout as triggered by the `timeout` shell function. + return 1; + } + + // The fuzzing module is required to handle any exceptions + CrashOnPendingException(); + + int32_t ret = 0; + if (!JS::ToInt32(gCx, v, &ret)) { + MOZ_CRASH("Must return an int32 compatible return value!"); + } + + return ret; +} diff --git a/js/src/shell/jsrtfuzzing/jsrtfuzzing.h b/js/src/shell/jsrtfuzzing/jsrtfuzzing.h new file mode 100644 index 0000000000..9216aa91a1 --- /dev/null +++ b/js/src/shell/jsrtfuzzing/jsrtfuzzing.h @@ -0,0 +1,28 @@ +/* -*- 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/. */ + +// jsrtfuzzing.h - Functionality for JS runtime fuzzing + +#ifndef shell_jsrtfuzzing_h +#define shell_jsrtfuzzing_h + +#include "vm/JSContext.h" + +namespace js { +namespace shell { + +// This is the entry point of the JS runtime fuzzing code from the JS shell +int FuzzJSRuntimeStart(JSContext* cx, int* argc, char*** argv); + +// These are the traditional libFuzzer-style functions for initialization +// and fuzzing iteration. +int FuzzJSRuntimeInit(int* argc, char*** argv); +int FuzzJSRuntimeFuzz(const uint8_t* buf, size_t size); + +} // namespace shell +} // namespace js + +#endif /* shell_jsrtfuzzing_h */ diff --git a/js/src/shell/jsshell.cpp b/js/src/shell/jsshell.cpp new file mode 100644 index 0000000000..fe4b7488dc --- /dev/null +++ b/js/src/shell/jsshell.cpp @@ -0,0 +1,128 @@ +/* -*- 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/. */ + +// jsshell.cpp - Utilities for the JS shell + +#include "shell/jsshell.h" + +#include "mozilla/Sprintf.h" + +#include "jsfriendapi.h" + +#include "js/GlobalObject.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetProperty, JS_GetPropertyById +#include "util/StringBuffer.h" + +using namespace JS; + +namespace js { +namespace shell { + +// Generate 'usage' and 'help' properties for the given object. +// JS_DefineFunctionsWithHelp will define individual function objects with both +// of those properties (eg getpid.usage = "getpid()" and getpid.help = "return +// the process id"). This function will generate strings for an "interface +// object", eg os.file, which contains some number of functions. +// +// .usage will be set to "<name> - interface object". +// +// .help will be set to a newline-separated list of functions that have either +// 'help' or 'usage' properties. Functions are described with their usage +// strings, if they have them, else with just their names. +// +bool GenerateInterfaceHelp(JSContext* cx, HandleObject obj, const char* name) { + RootedIdVector idv(cx); + if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY | JSITER_HIDDEN, &idv)) { + return false; + } + + JSStringBuilder buf(cx); + int numEntries = 0; + for (size_t i = 0; i < idv.length(); i++) { + RootedId id(cx, idv[i]); + RootedValue v(cx); + if (!JS_GetPropertyById(cx, obj, id, &v)) { + return false; + } + if (!v.isObject()) { + continue; + } + RootedObject prop(cx, &v.toObject()); + + RootedValue usage(cx); + RootedValue help(cx); + if (!JS_GetProperty(cx, prop, "usage", &usage)) { + return false; + } + if (!JS_GetProperty(cx, prop, "help", &help)) { + return false; + } + if (!usage.isString() && !help.isString()) { + continue; + } + + if (numEntries && !buf.append("\n")) { + return false; + } + numEntries++; + + if (!buf.append(" ")) { + return false; + } + + if (!buf.append(usage.isString() ? usage.toString() : id.toString())) { + return false; + } + } + + RootedString s(cx, buf.finishString()); + if (!s || !JS_DefineProperty(cx, obj, "help", s, 0)) { + return false; + } + + buf.clear(); + if (!buf.append(name, strlen(name)) || + !buf.append(" - interface object with ")) { + return false; + } + char cbuf[100]; + SprintfLiteral(cbuf, "%d %s", numEntries, + numEntries == 1 ? "entry" : "entries"); + if (!buf.append(cbuf, strlen(cbuf))) { + return false; + } + s = buf.finishString(); + if (!s || !JS_DefineProperty(cx, obj, "usage", s, 0)) { + return false; + } + + return true; +} + +bool CreateAlias(JSContext* cx, const char* dstName, + JS::HandleObject namespaceObj, const char* srcName) { + RootedObject global(cx, JS::GetNonCCWObjectGlobal(namespaceObj)); + + RootedValue val(cx); + if (!JS_GetProperty(cx, namespaceObj, srcName, &val)) { + return false; + } + + if (!val.isObject()) { + JS_ReportErrorASCII(cx, "attempted to alias nonexistent function"); + return false; + } + + RootedObject function(cx, &val.toObject()); + if (!JS_DefineProperty(cx, global, dstName, function, 0)) { + return false; + } + + return true; +} + +} // namespace shell +} // namespace js diff --git a/js/src/shell/jsshell.h b/js/src/shell/jsshell.h new file mode 100644 index 0000000000..e6566a6da2 --- /dev/null +++ b/js/src/shell/jsshell.h @@ -0,0 +1,263 @@ +/* -*- 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 jsshell_js_h +#define jsshell_js_h + +#include "mozilla/Atomics.h" +#include "mozilla/Maybe.h" +#include "mozilla/TimeStamp.h" + +#include "builtin/MapObject.h" +#include "js/CompileOptions.h" +#include "js/GCVector.h" +#include "shell/ModuleLoader.h" +#include "threading/ConditionVariable.h" +#include "threading/LockGuard.h" +#include "threading/Mutex.h" +#include "threading/Thread.h" +#include "vm/GeckoProfiler.h" +#include "vm/Monitor.h" + +// Some platform hooks must be implemented for single-step profiling. +#if defined(JS_SIMULATOR_ARM) || defined(JS_SIMULATOR_MIPS64) || \ + defined(JS_SIMULATOR_MIPS32) +# define SINGLESTEP_PROFILING +#endif + +namespace js { +namespace shell { + +// Define use of application-specific slots on the shell's global object. +enum GlobalAppSlot { GlobalAppSlotModuleRegistry, GlobalAppSlotCount }; +static_assert(GlobalAppSlotCount <= JSCLASS_GLOBAL_APPLICATION_SLOTS, + "Too many applications slots defined for shell global"); + +enum JSShellErrNum { +#define MSG_DEF(name, count, exception, format) name, +#include "jsshell.msg" +#undef MSG_DEF + JSShellErr_Limit +}; + +const JSErrorFormatString* my_GetErrorMessage(void* userRef, + const unsigned errorNumber); + +void WarningReporter(JSContext* cx, JSErrorReport* report); + +class MOZ_STACK_CLASS AutoReportException { + JSContext* cx; + + public: + explicit AutoReportException(JSContext* cx) : cx(cx) {} + ~AutoReportException(); +}; + +bool GenerateInterfaceHelp(JSContext* cx, JS::HandleObject obj, + const char* name); + +JSString* FileAsString(JSContext* cx, JS::HandleString pathnameStr); + +class AutoCloseFile { + private: + FILE* f_; + + public: + explicit AutoCloseFile(FILE* f) : f_(f) {} + ~AutoCloseFile() { (void)release(); } + bool release() { + bool success = true; + if (f_ && f_ != stdin && f_ != stdout && f_ != stderr) { + success = !fclose(f_); + } + f_ = nullptr; + return success; + } +}; + +// Reference counted file. +struct RCFile { + FILE* fp; + uint32_t numRefs; + + RCFile() : fp(nullptr), numRefs(0) {} + explicit RCFile(FILE* fp) : fp(fp), numRefs(0) {} + + void acquire() { numRefs++; } + + // Starts out with a ref count of zero. + static RCFile* create(JSContext* cx, const char* filename, const char* mode); + + void close(); + bool isOpen() const { return fp; } + bool release(); +}; + +// Shell command-line arguments and count. +extern int sArgc; +extern char** sArgv; + +// Shell state set once at startup. +extern const char* selfHostedXDRPath; +extern bool encodeSelfHostedCode; +extern bool enableCodeCoverage; +extern bool enableDisassemblyDumps; +extern bool offthreadCompilation; +extern JS::DelazificationOption defaultDelazificationMode; +extern bool enableAsmJS; +extern bool enableWasm; +extern bool enableSharedMemory; +extern bool enableWasmBaseline; +extern bool enableWasmOptimizing; + +#define WASM_FEATURE(NAME, ...) extern bool enableWasm##NAME; +JS_FOR_WASM_FEATURES(WASM_FEATURE, WASM_FEATURE, WASM_FEATURE); +#undef WASM_FEATURE + +extern bool enableWasmVerbose; +extern bool enableTestWasmAwaitTier2; +extern bool enableSourcePragmas; +extern bool enableAsyncStacks; +extern bool enableAsyncStackCaptureDebuggeeOnly; +extern bool enableWeakRefs; +extern bool enableToSource; +extern bool enablePropertyErrorMessageFix; +extern bool enableIteratorHelpers; +extern bool enableShadowRealms; +extern bool enableArrayGrouping; +extern bool enableArrayFromAsync; +extern bool enableWellFormedUnicodeStrings; +extern bool enablePrivateClassFields; +extern bool enablePrivateClassMethods; +extern bool enableChangeArrayByCopy; +#ifdef ENABLE_NEW_SET_METHODS +extern bool enableNewSetMethods; +#endif +extern bool enableClassStaticBlocks; +extern bool enableImportAssertions; +#ifdef JS_GC_ZEAL +extern uint32_t gZealBits; +extern uint32_t gZealFrequency; +#endif +extern bool printTiming; +extern RCFile* gErrFile; +extern RCFile* gOutFile; +extern bool reportWarnings; +extern bool compileOnly; +extern bool disableOOMFunctions; +extern bool defaultToSameCompartment; + +#ifdef DEBUG +extern bool dumpEntrainedVariables; +extern bool OOM_printAllocationCount; +#endif + +extern bool useFdlibmForSinCosTan; + +extern UniqueChars processWideModuleLoadPath; + +// Alias the global dstName to namespaceObj.srcName. For example, if dstName is +// "snarf", namespaceObj represents "os.file", and srcName is "readFile", then +// this is equivalent to the JS code: +// +// snarf = os.file.readFile; +// +// This provides a mechanism for namespacing the various JS shell helper +// functions without breaking backwards compatibility with things that use the +// global names. +bool CreateAlias(JSContext* cx, const char* dstName, + JS::HandleObject namespaceObj, const char* srcName); + +class NonshrinkingGCObjectVector + : public GCVector<HeapPtr<JSObject*>, 0, SystemAllocPolicy> { + public: + bool traceWeak(JSTracer* trc) { + for (HeapPtr<JSObject*>& obj : *this) { + TraceWeakEdge(trc, &obj, "NonshrinkingGCObjectVector element"); + } + return true; + } +}; + +using MarkBitObservers = JS::WeakCache<NonshrinkingGCObjectVector>; + +#ifdef SINGLESTEP_PROFILING +using StackChars = Vector<char16_t, 0, SystemAllocPolicy>; +#endif + +class OffThreadJob; + +// Per-context shell state. +struct ShellContext { + explicit ShellContext(JSContext* cx); + ~ShellContext(); + + bool isWorker; + bool lastWarningEnabled; + + // Track promise rejections and report unhandled rejections. + bool trackUnhandledRejections; + + double timeoutInterval; + double startTime; + mozilla::Atomic<bool> serviceInterrupt; + mozilla::Atomic<bool> haveInterruptFunc; + JS::PersistentRootedValue interruptFunc; + JS::PersistentRootedValue lastWarning; + JS::PersistentRootedValue promiseRejectionTrackerCallback; + + // Rejected promises that are not yet handled. Added when rejection + // happens, and removed when rejection is handled. This uses SetObject to + // report unhandled rejections in the rejected order. + JS::PersistentRooted<SetObject*> unhandledRejectedPromises; + +#ifdef SINGLESTEP_PROFILING + Vector<StackChars, 0, SystemAllocPolicy> stacks; +#endif + + /* + * Watchdog thread state. + */ + js::Mutex watchdogLock MOZ_UNANNOTATED; + js::ConditionVariable watchdogWakeup; + mozilla::Maybe<js::Thread> watchdogThread; + mozilla::Maybe<mozilla::TimeStamp> watchdogTimeout; + + js::ConditionVariable sleepWakeup; + + int exitCode; + bool quitting; + + JS::UniqueChars readLineBuf; + size_t readLineBufPos; + + js::shell::RCFile** errFilePtr; + js::shell::RCFile** outFilePtr; + + UniquePtr<ProfilingStack> geckoProfilingStack; + + UniquePtr<ModuleLoader> moduleLoader; + + UniquePtr<MarkBitObservers> markObservers; + + // Off-thread parse state. + js::Monitor offThreadMonitor MOZ_UNANNOTATED; + Vector<OffThreadJob*, 0, SystemAllocPolicy> offThreadJobs; + + // Queued finalization registry cleanup jobs. + using FunctionVector = GCVector<JSFunction*, 0, SystemAllocPolicy>; + JS::PersistentRooted<FunctionVector> finalizationRegistryCleanupCallbacks; +}; + +extern ShellContext* GetShellContext(JSContext* cx); + +[[nodiscard]] extern bool PrintStackTrace(JSContext* cx, + JS::Handle<JSObject*> stackObj); + +} /* namespace shell */ +} /* namespace js */ + +#endif diff --git a/js/src/shell/moz.build b/js/src/shell/moz.build new file mode 100644 index 0000000000..ed8551f1ec --- /dev/null +++ b/js/src/shell/moz.build @@ -0,0 +1,59 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +GeckoProgram("js", linkage=None) +if CONFIG["JS_BUNDLED_EDITLINE"]: + DIRS += ["../editline"] + USE_LIBS += ["editline"] +USE_LIBS += ["static:js"] + +include("../js-config.mozbuild") +include("../js-cxxflags.mozbuild") +include("../js-standalone.mozbuild") + +UNIFIED_SOURCES += [ + "js.cpp", + "jsoptparse.cpp", + "jsshell.cpp", + "ModuleLoader.cpp", + "OSObject.cpp", + "ShellModuleObjectWrapper.cpp", + "WasmTesting.cpp", +] + +if CONFIG["FUZZING_INTERFACES"]: + UNIFIED_SOURCES += ["jsrtfuzzing/jsrtfuzzing.cpp"] + USE_LIBS += [ + "static:fuzzer", + ] + +if CONFIG["FUZZING_JS_FUZZILLI"]: + OS_LIBS += ["rt"] + +DEFINES["EXPORT_JS_API"] = True + +LOCAL_INCLUDES += [ + "!..", + "..", +] + +OS_LIBS += CONFIG["EDITLINE_LIBS"] + +# Place a GDB Python auto-load file next to the shell executable, both in +# the build directory and in the dist/bin directory. +FINAL_TARGET_FILES += ["js-gdb.py"] +OBJDIR_FILES.js.src.shell += ["!/dist/bin/js-gdb.py"] + +# People expect the js shell to wind up in the top-level JS dir. +OBJDIR_FILES.js.src += ["!/dist/bin/js%s" % CONFIG["BIN_SUFFIX"]] + +# Stack size on Wasm/WASI +# ================================== +# Increase the default stack size (64KB) to 1MB. +# Also make the stack grow towards 0 so that if SpiderMonkey's stack limiter is buggy, overflow will likely trap. +if CONFIG["OS_ARCH"] == "WASI": + LDFLAGS += ["-Wl,-z,stack-size=1048576", "-Wl,--stack-first"] + OS_LIBS += ["wasi-emulated-process-clocks", "wasi-emulated-getpid"] |